diff --git a/.env.example b/.env.example
index bed835645756..944da2aa9296 100644
--- a/.env.example
+++ b/.env.example
@@ -11,7 +11,6 @@ USE_WEB_PROXY=false
USE_WDYR=false
CAPTURE_METRICS=false
ONYX_METRICS=false
-GOOGLE_GEOLOCATION_API_KEY=AIzaSyBqg6bMvQU7cPWDKhhzpYqJrTEnSorpiLI
EXPENSIFY_ACCOUNT_ID_ACCOUNTING=-1
EXPENSIFY_ACCOUNT_ID_ADMIN=-1
diff --git a/.env.production b/.env.production
index 4b0a98e77557..5e676134d681 100644
--- a/.env.production
+++ b/.env.production
@@ -7,4 +7,3 @@ PUSHER_APP_KEY=268df511a204fbb60884
USE_WEB_PROXY=false
ENVIRONMENT=production
SEND_CRASH_REPORTS=true
-GOOGLE_GEOLOCATION_API_KEY=AIzaSyBFKujMpzExz0_z2pAGfPUwkmlaUc-uw1Q
diff --git a/.env.staging b/.env.staging
index 1b3ec15fc172..17d82ac2d136 100644
--- a/.env.staging
+++ b/.env.staging
@@ -6,5 +6,4 @@ EXPENSIFY_PARTNER_PASSWORD=e21965746fd75f82bb66
PUSHER_APP_KEY=268df511a204fbb60884
USE_WEB_PROXY=false
ENVIRONMENT=staging
-SEND_CRASH_REPORTS=true
-GOOGLE_GEOLOCATION_API_KEY=AIzaSyD2T1mlByThbUN88O8OPOD8vKuMMwLD4-M
\ No newline at end of file
+SEND_CRASH_REPORTS=true
\ No newline at end of file
diff --git a/.github/workflows/platformDeploy.yml b/.github/workflows/platformDeploy.yml
index 353a898a941f..4ad6d54e2f24 100644
--- a/.github/workflows/platformDeploy.yml
+++ b/.github/workflows/platformDeploy.yml
@@ -366,6 +366,17 @@ jobs:
with:
SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK }}
+ # Build a version of iOS and Android HybridApp if we are deploying to staging
+ hybridApp:
+ runs-on: ubuntu-latest
+ needs: validateActor
+ if: ${{ fromJSON(needs.validateActor.outputs.IS_DEPLOYER) && github.event_name == 'push' }}
+ steps:
+ - name: 'Deploy HybridApp'
+ run: gh workflow run --repo Expensify/Mobile-Deploy deploy.yml -f force_build=true
+ env:
+ GITHUB_TOKEN: ${{ secrets.OS_BOTIFY_TOKEN }}
+
postSlackMessageOnSuccess:
name: Post a Slack message when all platforms deploy successfully
runs-on: ubuntu-latest
diff --git a/android/app/build.gradle b/android/app/build.gradle
index 229b0c258259..e107bcc6832b 100644
--- a/android/app/build.gradle
+++ b/android/app/build.gradle
@@ -107,8 +107,8 @@ android {
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
multiDexEnabled rootProject.ext.multiDexEnabled
- versionCode 1001047906
- versionName "1.4.79-6"
+ versionCode 1001047911
+ versionName "1.4.79-11"
// Supported language variants must be declared here to avoid from being removed during the compilation.
// This also helps us to not include unnecessary language variants in the APK.
resConfigs "en", "es"
diff --git a/assets/images/product-illustrations/emptystate__travel.svg b/assets/images/product-illustrations/emptystate__travel.svg
new file mode 100644
index 000000000000..416b27eb5bee
--- /dev/null
+++ b/assets/images/product-illustrations/emptystate__travel.svg
@@ -0,0 +1,575 @@
+
+
+
diff --git a/assets/images/receipt-slash.svg b/assets/images/receipt-slash.svg
new file mode 100644
index 000000000000..2af3fcbc60e6
--- /dev/null
+++ b/assets/images/receipt-slash.svg
@@ -0,0 +1,12 @@
+
\ No newline at end of file
diff --git a/docs/articles/expensify-classic/workspaces/reports/Report-Fields-And-Titles.md b/docs/articles/expensify-classic/workspaces/reports/Report-Fields-And-Titles.md
deleted file mode 100644
index e79e30ce42c9..000000000000
--- a/docs/articles/expensify-classic/workspaces/reports/Report-Fields-And-Titles.md
+++ /dev/null
@@ -1,43 +0,0 @@
----
-title: Report Fields & Titles
-description: This article is about managing Report Fields and Report Titles in Expensify
----
-# Overview
-
-In this article, we'll go over how to use Report Titles and Report Fields.
-
-## How to use Report Titles
-
-Default report titles enable group workspace admins or individual workspace users to establish a standardized title format for reports associated with a specific workspace. Additionally, admins have the choice to enforce these report titles, preventing employees from altering them. This ensures uniformity in report naming conventions during the review process, eliminating the need for employees to manually input titles for each report they generate.
-
-- Group workspace admins can set the Default Report Title from **Settings > Workspaces > Group > *[Workspace Name]* > Reports**.
-- Individual users can set the Default Report Title from **Settings > Workspaces > Individual > *[Workspace Name]* > Reports**.
-
-You can configure the title by using the formulas that we provide to populate the Report Title. Take a look at the help article on Custom Formulas to find all eligible formulas for your Report Titles.
-
-## Deep Dive on Report Titles
-
-Some formulas will automatically update the report title as changes are made to the report. For example, any formula related to dates, total amounts, workspace name, would adjust the title before the report is submitted for approval. Changes will not retroactively update report titles for reports which have been Approved or Reimbursed.
-
-To prevent report title editing by employees, simply enable "Enforce Default Report Title."
-
-## How to use Report Fields
-
-Report fields let you specify header-level details, distinct from tags which pertain to expenses on individual line items. These details can encompass specific project names, business trip information, locations, and more. Customize them according to your workspace's requirements.
-
-To set up Report Fields, follow these steps:
-- Workspace Admins can create report fields for group workspaces from **Settings > Workspaces > Group > *[Workspace Name]* > Reports > Report and Invoice Fields**. For individual workspaces, follow **Settings > Workspaces > Individual > *[Workspace Name]* > Reports > Report and Invoice Fields**.
-- Under "Add New Field," enter the desired field name in the "Field Title" to describe the type of information to be selected.
-- Choose the appropriate input method under "Type":
- - Text: Provides users with a free-text box to enter the requested information.
- - Dropdown: Creates a selection of options for users to choose from.
- - Date: Displays a clickable box that opens a calendar for users to select a date.
-
-## Deep Dive on Report Fields
-
-You cannot create these report fields directly in Expensify if you are connected to an accounting integration (QuickBooks Online, QuickBooks Desktop, Intacct, Xero, or NetSuite). Please refer to the relevant article for instructions on creating fields within that system.
-
-When report fields are configured on a workspace, they become mandatory information for associated reports. Leaving a report field empty or unselected will trigger a report violation, potentially blocking report submission or export.
-
-Report fields are "sticky," which means that any changes made by an employee will persist and be reflected in subsequent reports they create.
-
diff --git a/docs/articles/expensify-classic/workspaces/reports/Scheduled-Submit.md b/docs/articles/expensify-classic/workspaces/reports/Scheduled-Submit.md
deleted file mode 100644
index 18ad693a1c56..000000000000
--- a/docs/articles/expensify-classic/workspaces/reports/Scheduled-Submit.md
+++ /dev/null
@@ -1,43 +0,0 @@
----
-title: Scheduled Submit
-description: How to use the Scheduled Submit feature
----
-# Overview
-
-Scheduled Submit reduces the delay between the time an employee creates an expense to when it is submitted to the admin. This gives admins significantly faster visibility into employee spend. Without Scheduled Submit enabled, expenses can be left Unreported giving workspace admins no visibility into employee spend.
-
-The biggest delay in expense management is the time it takes for an employee to actually submit the expense after it is incurred. Scheduled Submit allows you to automatically collect employee expenses on a schedule of your choosing without delaying the process while you wait for employees to submit them.
-
-It works like this: Employee expenses are automatically gathered onto a report. If there is not an existing report, a new one will be created. This report is submitted automatically at the cadence you choose (daily, weekly, monthly, twice month, by trip).
-
-# How to enable Scheduled Submit
-
-**For workspace admins**: To enable Scheduled Submit on your group workspace, follow **Settings > Workspaces > Group > *[Workspace Name]* > Reports > Scheduled Submit**. From there, toggle Scheduled Submit to Enabled. Then, choose your desired frequency from the dropdown menu.
-For individuals or employees: To enable Scheduled Submit on your individual workspace, follow **Settings > Workspaces > Individual > *[Workspace Name]* > Reports > Scheduled Submit**. From there, toggle Scheduled Submit to Enabled. Then, choose your desired frequency from the dropdown menu.
-
-## Scheduled Submit frequency options
-
-**Daily**: Each night, expenses without violations will be submitted. Expenses with violations will remain on an open report until the violations are corrected, after which they will be submitted in the evening (PDT).
-
-**Weekly**: Expenses that are free of violations will be submitted on a weekly basis. However, expenses with violations will be held in a new open report and combined with any new expenses. They will then be submitted at the end of the following weekly cycle, specifically on Sunday evening (PDT).
-
-**Twice a month**: Expenses that are violation-free will be submitted on both the 15th and the last day of each month, in the evening (PDT). Expenses with violations will not be submitted, but moved on to a new open report so the employee can resolve the violations and then will be submitted at the conclusion of the next cycle.
-
-**Monthly**: Expenses that are free from violations will be submitted on a monthly basis. Expenses with violations will be held back and moved to a new Open report so the violations can be resolved, and they will be submitted on the evening (PDT) of the specified date.
-
-**By trip**: Expenses are grouped by trip. This is calculated by grouping all expenses together that occur in a similar time frame. If two full days pass without any new expenses being created, the trip report will be submitted on the evening of the second day. Any expenses generated after this submission will initiate a new trip report. Please note that the "2-day" period refers to a date-based interval, not a 48-hour time frame.
-
-**Manually**: An open report will be created, and expenses will be added to it automatically. However, it's important to note that the report will not be submitted automatically; manual submission of reports will be required.This is a great option for automatically gathering all an employee’s expenses on a report for the employee’s convenience, but they will still need to review and submit the report.
-
-**Instantly**: Expenses are automatically added to a report in the Processing state, and all expenses will continue to accumulate on one report until it is Approved or Reimbursed. This removes the need to submit expenses, and Processing reports can either be Reimbursed right away (if Submit and Close is enabled), or Approved and then Reimbursed (if Submit and Approve is enabled) by a workspace admin.
-
-# Deep Dive
-
-## Schedule Submit Override
-If Scheduled Submit is disabled at the group workspace level or configured the frequency as "Manually," the individual workspace settings of a user will take precedence and be applied. This means an employee can still set up Scheduled Submit for themselves even if the admin has not enabled it. We highly recommend Scheduled Submit as it helps put your expenses on auto-pilot!
-
-## Personal Card Transactions
-Personal card transactions are handled differently compared to other expenses. If a user has added a card through Settings > Account > Credit Card Import, they need to make sure it is set as non-reimbursable and transactions must be automatically merged with a SmartScanned receipt. If transactions are set to come in as reimbursable or they aren’t merged with a SmartScanned receipt, Scheduled Submit settings will not apply.
-
-## A note on Instantly
-Setting Scheduled Submit frequency to Instantly will limit some employee actions on reports, such as the ability to retract or self-close reports, or create multiple reports. When Instantly is selected, expenses are automatically added to a Processing expense report, and new expenses will continue to accumulate on a single report until the report is Closed or Reimbursed by a workspace admin.
diff --git a/docs/redirects.csv b/docs/redirects.csv
index 3042dc79085c..13463327d06d 100644
--- a/docs/redirects.csv
+++ b/docs/redirects.csv
@@ -197,5 +197,7 @@ https://help.expensify.com/articles/new-expensify/workspaces/The-Free-Plan,https
https://help.expensify.com/new-expensify/hubs/expenses/Connect-a-Bank-Account,https://help.expensify.com/articles/new-expensify/expenses/Connect-a-Business-Bank-Account
https://help.expensify.com/articles/new-expensify/settings/Security,https://help.expensify.com/articles/new-expensify/settings/Encryption-and-Data-Security
https://help.expensify.com/articles/expensify-classic/workspaces/reports/Currency,https://help.expensify.com/articles/expensify-classic/workspaces/Currency
+https://help.expensify.com/articles/expensify-classic/workspaces/reports/Report-Fields-And-Titles,https://help.expensify.com/expensify-classic/hubs/workspaces/
+https://help.expensify.com/articles/expensify-classic/workspaces/reports/Scheduled-Submit,https://help.expensify.com/articles/expensify-classic/reports/Automatically-submit-employee-reports
https://help.expensify.com/articles/new-expensify/chat/Expensify-Chat-For-Admins,https://help.expensify.com/new-expensify/hubs/chat/
https://help.expensify.com/articles/new-expensify/bank-accounts-and-payments/Connect-a-Bank-Account.html,https://help.expensify.com/articles/new-expensify/expenses/Connect-a-Business-Bank-Account
diff --git a/ios/NewExpensify/Info.plist b/ios/NewExpensify/Info.plist
index 70b528c58abd..0e5f569f0363 100644
--- a/ios/NewExpensify/Info.plist
+++ b/ios/NewExpensify/Info.plist
@@ -40,7 +40,7 @@
CFBundleVersion
- 1.4.79.6
+ 1.4.79.11
FullStory
OrgId
diff --git a/ios/NewExpensifyTests/Info.plist b/ios/NewExpensifyTests/Info.plist
index 22fadffa18f9..901ebdb4b2ea 100644
--- a/ios/NewExpensifyTests/Info.plist
+++ b/ios/NewExpensifyTests/Info.plist
@@ -19,6 +19,6 @@
CFBundleSignature
????
CFBundleVersion
- 1.4.79.6
+ 1.4.79.11
diff --git a/ios/NotificationServiceExtension/Info.plist b/ios/NotificationServiceExtension/Info.plist
index 0e081da35219..a89a48f4bab3 100644
--- a/ios/NotificationServiceExtension/Info.plist
+++ b/ios/NotificationServiceExtension/Info.plist
@@ -13,7 +13,7 @@
CFBundleShortVersionString
1.4.79
CFBundleVersion
- 1.4.79.6
+ 1.4.79.11
NSExtension
NSExtensionPointIdentifier
diff --git a/ios/Podfile.lock b/ios/Podfile.lock
index 1ea6b65a58b7..de363c211cb3 100644
--- a/ios/Podfile.lock
+++ b/ios/Podfile.lock
@@ -1942,7 +1942,7 @@ PODS:
- React-Codegen
- React-Core
- ReactCommon/turbomodule/core
- - RNReanimated (3.7.2):
+ - RNReanimated (3.8.1):
- glog
- hermes-engine
- RCT-Folly (= 2022.05.16.00)
@@ -2594,7 +2594,7 @@ SPEC CHECKSUMS:
rnmapbox-maps: df8fe93dbd251f25022f4023d31bc04160d4d65c
RNPermissions: 0b61d30d21acbeafe25baaa47d9bae40a0c65216
RNReactNativeHapticFeedback: 616c35bdec7d20d4c524a7949ca9829c09e35f37
- RNReanimated: 51db0fff543694d931bd3b7cab1a3b36bd86c738
+ RNReanimated: 323436b1a5364dca3b5f8b1a13458455e0de9efe
RNScreens: 9ec969a95987a6caae170ef09313138abf3331e1
RNShare: 2a4cdfc0626ad56b0ef583d424f2038f772afe58
RNSound: 6c156f925295bdc83e8e422e7d8b38d33bc71852
diff --git a/package-lock.json b/package-lock.json
index afcc810b36be..801d0f68cd3c 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "new.expensify",
- "version": "1.4.79-6",
+ "version": "1.4.79-11",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "new.expensify",
- "version": "1.4.79-6",
+ "version": "1.4.79-11",
"hasInstallScript": true,
"license": "MIT",
"dependencies": {
@@ -59,7 +59,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#1713f28214f0e7176c4fd13433fb0ea15491ebf9",
+ "expensify-common": "^2.0.10",
"expo": "^50.0.3",
"expo-av": "~13.10.4",
"expo-image": "1.11.0",
@@ -19842,8 +19842,9 @@
},
"node_modules/eslint-plugin-you-dont-need-lodash-underscore": {
"version": "6.12.0",
+ "resolved": "https://registry.npmjs.org/eslint-plugin-you-dont-need-lodash-underscore/-/eslint-plugin-you-dont-need-lodash-underscore-6.12.0.tgz",
+ "integrity": "sha512-WF4mNp+k2532iswT6iUd1BX6qjd3AV4cFy/09VC82GY9SsRtvkxhUIx7JNGSe0/bLyd57oTr4inPFiIaENXhGw==",
"dev": true,
- "license": "MIT",
"dependencies": {
"kebab-case": "^1.0.0"
},
@@ -20335,11 +20336,11 @@
}
},
"node_modules/expensify-common": {
- "version": "1.0.0",
- "resolved": "git+ssh://git@github.com/Expensify/expensify-common.git#1713f28214f0e7176c4fd13433fb0ea15491ebf9",
- "integrity": "sha512-uy1+axUTTuPKwAR06xNG/tGIJ+uaavmSQgKiNU7pQVR94ibNzDD2WESn2E7OEP9/QrHa61lfFlluTjFvvz5I8Q==",
- "license": "MIT",
+ "version": "2.0.10",
+ "resolved": "https://registry.npmjs.org/expensify-common/-/expensify-common-2.0.10.tgz",
+ "integrity": "sha512-+8LCtnR+VxmCjKKkfeR6XGAhVxvwZtQAw3386c1EDGNK1C0bvz3I1kLVMFbulSeibZv6/G33aO6SiW/kwum6Nw==",
"dependencies": {
+ "awesome-phonenumber": "^5.4.0",
"classnames": "2.5.0",
"clipboard": "2.0.11",
"html-entities": "^2.5.2",
diff --git a/package.json b/package.json
index 48bff8c93838..940d4eb091a1 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "new.expensify",
- "version": "1.4.79-6",
+ "version": "1.4.79-11",
"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.",
@@ -111,7 +111,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#1713f28214f0e7176c4fd13433fb0ea15491ebf9",
+ "expensify-common": "^2.0.10",
"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 bc8f627630a3..a4c330b36cc6 100755
--- a/src/CONST.ts
+++ b/src/CONST.ts
@@ -940,7 +940,7 @@ const CONST = {
TO: 'to',
CATEGORY: 'category',
TAG: 'tag',
- TOTAL: 'total',
+ TOTAL_AMOUNT: 'amount',
TYPE: 'type',
ACTION: 'action',
TAX_AMOUNT: 'taxAmount',
@@ -1817,6 +1817,7 @@ const CONST = {
XERO_CHECK_CONNECTION: 'xeroCheckConnection',
XERO_SYNC_TITLE: 'xeroSyncTitle',
},
+ SYNC_STAGE_TIMEOUT_MINUTES: 20,
},
ACCESS_VARIANTS: {
PAID: 'paid',
diff --git a/src/ONYXKEYS.ts b/src/ONYXKEYS.ts
index f8600ed9c9ee..85744329d487 100755
--- a/src/ONYXKEYS.ts
+++ b/src/ONYXKEYS.ts
@@ -397,6 +397,8 @@ const ONYXKEYS = {
POLICY_CREATE_DISTANCE_RATE_FORM: 'policyCreateDistanceRateForm',
POLICY_CREATE_DISTANCE_RATE_FORM_DRAFT: 'policyCreateDistanceRateFormDraft',
POLICY_DISTANCE_RATE_EDIT_FORM: 'policyDistanceRateEditForm',
+ POLICY_DISTANCE_RATE_TAX_RECLAIMABLE_ON_EDIT_FORM: 'policyDistanceRateTaxReclaimableOnEditForm',
+ POLICY_DISTANCE_RATE_TAX_RECLAIMABLE_ON_EDIT_FORM_DRAFT: 'policyDistanceRateTaxReclaimableOnEditFormDraft',
POLICY_DISTANCE_RATE_EDIT_FORM_DRAFT: 'policyDistanceRateEditFormDraft',
CLOSE_ACCOUNT_FORM: 'closeAccount',
CLOSE_ACCOUNT_FORM_DRAFT: 'closeAccountDraft',
@@ -538,6 +540,7 @@ type OnyxFormValuesMapping = {
[ONYXKEYS.FORMS.WORKSPACE_NEW_TAX_FORM]: FormTypes.WorkspaceNewTaxForm;
[ONYXKEYS.FORMS.POLICY_CREATE_DISTANCE_RATE_FORM]: FormTypes.PolicyCreateDistanceRateForm;
[ONYXKEYS.FORMS.POLICY_DISTANCE_RATE_EDIT_FORM]: FormTypes.PolicyDistanceRateEditForm;
+ [ONYXKEYS.FORMS.POLICY_DISTANCE_RATE_TAX_RECLAIMABLE_ON_EDIT_FORM]: FormTypes.PolicyDistanceRateTaxReclaimableOnEditForm;
[ONYXKEYS.FORMS.WORKSPACE_TAX_NAME_FORM]: FormTypes.WorkspaceTaxNameForm;
[ONYXKEYS.FORMS.WORKSPACE_TAX_VALUE_FORM]: FormTypes.WorkspaceTaxValueForm;
[ONYXKEYS.FORMS.NEW_CHAT_NAME_FORM]: FormTypes.NewChatNameForm;
diff --git a/src/ROUTES.ts b/src/ROUTES.ts
index 61034382fefd..5a8f4a2cd4d0 100644
--- a/src/ROUTES.ts
+++ b/src/ROUTES.ts
@@ -103,6 +103,7 @@ const ROUTES = {
SETTINGS_PREFERENCES: 'settings/preferences',
SETTINGS_SUBSCRIPTION: 'settings/subscription',
SETTINGS_SUBSCRIPTION_SIZE: 'settings/subscription/subscription-size',
+ SETTINGS_SUBSCRIPTION_ADD_PAYMENT_CARD: 'settings/subscription/add-payment-card',
SETTINGS_PRIORITY_MODE: 'settings/preferences/priority-mode',
SETTINGS_LANGUAGE: 'settings/preferences/language',
SETTINGS_THEME: 'settings/preferences/theme',
@@ -796,6 +797,14 @@ const ROUTES = {
route: 'settings/workspaces/:policyID/distance-rates/:rateID/edit',
getRoute: (policyID: string, rateID: string) => `settings/workspaces/${policyID}/distance-rates/${rateID}/edit` as const,
},
+ WORKSPACE_DISTANCE_RATE_TAX_RECLAIMABLE_ON_EDIT: {
+ route: 'settings/workspaces/:policyID/distance-rates/:rateID/tax-reclaimable/edit',
+ getRoute: (policyID: string, rateID: string) => `settings/workspaces/${policyID}/distance-rates/${rateID}/tax-reclaimable/edit` as const,
+ },
+ WORKSPACE_DISTANCE_RATE_TAX_RATE_EDIT: {
+ route: 'settings/workspaces/:policyID/distance-rates/:rateID/tax-rate/edit',
+ getRoute: (policyID: string, rateID: string) => `settings/workspaces/${policyID}/distance-rates/${rateID}/tax-rate/edit` as const,
+ },
// Referral program promotion
REFERRAL_DETAILS_MODAL: {
route: 'referral/:contentType',
diff --git a/src/SCREENS.ts b/src/SCREENS.ts
index 6f32f980d6c2..fd7418aee1c5 100644
--- a/src/SCREENS.ts
+++ b/src/SCREENS.ts
@@ -107,6 +107,7 @@ const SCREENS = {
SUBSCRIPTION: {
ROOT: 'Settings_Subscription',
SIZE: 'Settings_Subscription_Size',
+ ADD_PAYMENT_CARD: 'Settings_Subscription_Add_Payment_Card',
},
},
SAVE_THE_WORLD: {
@@ -321,6 +322,8 @@ const SCREENS = {
DISTANCE_RATES_SETTINGS: 'Distance_Rates_Settings',
DISTANCE_RATE_DETAILS: 'Distance_Rate_Details',
DISTANCE_RATE_EDIT: 'Distance_Rate_Edit',
+ DISTANCE_RATE_TAX_RECLAIMABLE_ON_EDIT: 'Distance_Rate_Tax_Reclaimable_On_Edit',
+ DISTANCE_RATE_TAX_RATE_EDIT: 'Distance_Rate_Tax_Rate_Edit',
},
EDIT_REQUEST: {
diff --git a/src/pages/workspace/members/WorkspaceOwnerPaymentCardCurrencyModal.tsx b/src/components/AddPaymentCard/PaymentCardCurrencyModal.tsx
similarity index 82%
rename from src/pages/workspace/members/WorkspaceOwnerPaymentCardCurrencyModal.tsx
rename to src/components/AddPaymentCard/PaymentCardCurrencyModal.tsx
index fcbbbbd4af3f..60fa838b0577 100644
--- a/src/pages/workspace/members/WorkspaceOwnerPaymentCardCurrencyModal.tsx
+++ b/src/components/AddPaymentCard/PaymentCardCurrencyModal.tsx
@@ -6,9 +6,10 @@ import SelectionList from '@components/SelectionList';
import RadioListItem from '@components/SelectionList/RadioListItem';
import useLocalize from '@hooks/useLocalize';
import useThemeStyles from '@hooks/useThemeStyles';
+import useWindowDimensions from '@hooks/useWindowDimensions';
import CONST from '@src/CONST';
-type WorkspaceOwnerPaymentCardCurrencyModalProps = {
+type PaymentCardCurrencyModalProps = {
/** Whether the modal is visible */
isVisible: boolean;
@@ -25,7 +26,8 @@ type WorkspaceOwnerPaymentCardCurrencyModalProps = {
onClose?: () => void;
};
-function WorkspaceOwnerPaymentCardCurrencyModal({isVisible, currencies, currentCurrency = CONST.CURRENCY.USD, onCurrencyChange, onClose}: WorkspaceOwnerPaymentCardCurrencyModalProps) {
+function PaymentCardCurrencyModal({isVisible, currencies, currentCurrency = CONST.CURRENCY.USD, onCurrencyChange, onClose}: PaymentCardCurrencyModalProps) {
+ const {isSmallScreenWidth} = useWindowDimensions();
const styles = useThemeStyles();
const {translate} = useLocalize();
const {sections} = useMemo(
@@ -51,13 +53,14 @@ function WorkspaceOwnerPaymentCardCurrencyModal({isVisible, currencies, currentC
onClose={() => onClose?.()}
onModalHide={onClose}
hideModalContentWhileAnimating
+ innerContainerStyle={styles.RHPNavigatorContainer(isSmallScreenWidth)}
useNativeDriver
>
, currency?: ValueOf) => void;
+ submitButtonText: string;
+ /** Custom content to display in the footer after card form */
+ footerContent?: ReactNode;
+ /** Custom content to display in the header before card form */
+ headerContent?: ReactNode;
+};
+
+function IAcceptTheLabel() {
+ const {translate} = useLocalize();
+
+ return (
+
+ {`${translate('common.iAcceptThe')}`}
+ {`${translate('common.addCardTermsOfService')}`} {`${translate('common.and')}`}
+ {` ${translate('common.privacyPolicy')} `}
+
+ );
+}
+
+const REQUIRED_FIELDS = [
+ INPUT_IDS.NAME_ON_CARD,
+ INPUT_IDS.CARD_NUMBER,
+ INPUT_IDS.EXPIRATION_DATE,
+ INPUT_IDS.ADDRESS_STREET,
+ INPUT_IDS.SECURITY_CODE,
+ INPUT_IDS.ADDRESS_ZIP_CODE,
+ INPUT_IDS.ADDRESS_STATE,
+];
+
+const CARD_TYPES = {
+ DEBIT_CARD: 'debit',
+ PAYMENT_CARD: 'payment',
+};
+
+const CARD_TYPE_SECTIONS = {
+ DEFAULTS: 'defaults',
+ ERROR: 'error',
+};
+type CartTypesMap = (typeof CARD_TYPES)[keyof typeof CARD_TYPES];
+type CartTypeSectionsMap = (typeof CARD_TYPE_SECTIONS)[keyof typeof CARD_TYPE_SECTIONS];
+
+type CardLabels = Record>>;
+
+const CARD_LABELS: CardLabels = {
+ [CARD_TYPES.DEBIT_CARD]: {
+ [CARD_TYPE_SECTIONS.DEFAULTS]: {
+ cardNumber: 'addDebitCardPage.debitCardNumber',
+ nameOnCard: 'addDebitCardPage.nameOnCard',
+ expirationDate: 'addDebitCardPage.expirationDate',
+ expiration: 'addDebitCardPage.expiration',
+ securityCode: 'addDebitCardPage.cvv',
+ billingAddress: 'addDebitCardPage.billingAddress',
+ },
+ [CARD_TYPE_SECTIONS.ERROR]: {
+ nameOnCard: 'addDebitCardPage.error.invalidName',
+ cardNumber: 'addDebitCardPage.error.debitCardNumber',
+ expirationDate: 'addDebitCardPage.error.expirationDate',
+ securityCode: 'addDebitCardPage.error.securityCode',
+ addressStreet: 'addDebitCardPage.error.addressStreet',
+ addressZipCode: 'addDebitCardPage.error.addressZipCode',
+ },
+ },
+ [CARD_TYPES.PAYMENT_CARD]: {
+ defaults: {
+ cardNumber: 'addPaymentCardPage.paymentCardNumber',
+ nameOnCard: 'addPaymentCardPage.nameOnCard',
+ expirationDate: 'addPaymentCardPage.expirationDate',
+ expiration: 'addPaymentCardPage.expiration',
+ securityCode: 'addPaymentCardPage.cvv',
+ billingAddress: 'addPaymentCardPage.billingAddress',
+ },
+ error: {
+ nameOnCard: 'addPaymentCardPage.error.invalidName',
+ cardNumber: 'addPaymentCardPage.error.paymentCardNumber',
+ expirationDate: 'addPaymentCardPage.error.expirationDate',
+ securityCode: 'addPaymentCardPage.error.securityCode',
+ addressStreet: 'addPaymentCardPage.error.addressStreet',
+ addressZipCode: 'addPaymentCardPage.error.addressZipCode',
+ },
+ },
+};
+
+function PaymentCardForm({
+ shouldShowPaymentCardForm,
+ addPaymentCard,
+ showAcceptTerms,
+ showAddressField,
+ showCurrencyField,
+ isDebitCard,
+ submitButtonText,
+ showStateSelector,
+ footerContent,
+ headerContent,
+}: PaymentCardFormProps) {
+ const styles = useThemeStyles();
+ const {translate} = useLocalize();
+ const route = useRoute();
+ const label = CARD_LABELS[isDebitCard ? CARD_TYPES.DEBIT_CARD : CARD_TYPES.PAYMENT_CARD];
+
+ const cardNumberRef = useRef(null);
+
+ const [isCurrencyModalVisible, setIsCurrencyModalVisible] = useState(false);
+ const [currency, setCurrency] = useState(CONST.CURRENCY.USD);
+
+ const validate = (formValues: FormOnyxValues): FormInputErrors => {
+ const errors = ValidationUtils.getFieldRequiredErrors(formValues, REQUIRED_FIELDS);
+
+ if (formValues.nameOnCard && !ValidationUtils.isValidLegalName(formValues.nameOnCard)) {
+ errors.nameOnCard = label.error.nameOnCard;
+ }
+
+ if (formValues.cardNumber && !ValidationUtils.isValidDebitCard(formValues.cardNumber.replace(/ /g, ''))) {
+ errors.cardNumber = label.error.cardNumber;
+ }
+
+ if (formValues.expirationDate && !ValidationUtils.isValidExpirationDate(formValues.expirationDate)) {
+ errors.expirationDate = label.error.expirationDate;
+ }
+
+ if (formValues.securityCode && !ValidationUtils.isValidSecurityCode(formValues.securityCode)) {
+ errors.securityCode = label.error.securityCode;
+ }
+
+ if (formValues.addressStreet && !ValidationUtils.isValidAddress(formValues.addressStreet)) {
+ errors.addressStreet = label.error.addressStreet;
+ }
+
+ if (formValues.addressZipCode && !ValidationUtils.isValidZipCode(formValues.addressZipCode)) {
+ errors.addressZipCode = label.error.addressZipCode;
+ }
+
+ if (!formValues.acceptTerms) {
+ errors.acceptTerms = 'common.error.acceptTerms';
+ }
+
+ return errors;
+ };
+
+ const showCurrenciesModal = useCallback(() => {
+ setIsCurrencyModalVisible(true);
+ }, []);
+
+ const changeCurrency = useCallback((newCurrency: keyof typeof CONST.CURRENCY) => {
+ setCurrency(newCurrency);
+ setIsCurrencyModalVisible(false);
+ }, []);
+
+ if (!shouldShowPaymentCardForm) {
+ return null;
+ }
+
+ return (
+ <>
+ {headerContent}
+ addPaymentCard(formData, currency)}
+ submitButtonText={submitButtonText}
+ scrollContextEnabled
+ style={[styles.mh5, styles.flexGrow1]}
+ >
+
+
+
+
+
+
+
+
+
+
+ {!!showAddressField && (
+
+
+
+ )}
+
+ {!!showStateSelector && (
+
+
+
+ )}
+ {!!showCurrencyField && (
+
+ {(isHovered) => (
+
+ )}
+
+ )}
+ {!!showAcceptTerms && (
+
+
+
+ )}
+
+ }
+ currentCurrency={currency}
+ onCurrencyChange={changeCurrency}
+ onClose={() => setIsCurrencyModalVisible(false)}
+ />
+ {footerContent}
+
+ >
+ );
+}
+
+PaymentCardForm.displayName = 'PaymentCardForm';
+
+export default PaymentCardForm;
diff --git a/src/components/AnchorForCommentsOnly/BaseAnchorForCommentsOnly.tsx b/src/components/AnchorForCommentsOnly/BaseAnchorForCommentsOnly.tsx
index 2212e7460a2a..4c470858292c 100644
--- a/src/components/AnchorForCommentsOnly/BaseAnchorForCommentsOnly.tsx
+++ b/src/components/AnchorForCommentsOnly/BaseAnchorForCommentsOnly.tsx
@@ -1,4 +1,4 @@
-import Str from 'expensify-common/lib/str';
+import {Str} from 'expensify-common';
import React, {useEffect, useRef} from 'react';
// eslint-disable-next-line no-restricted-imports
import type {Text as RNText} from 'react-native';
diff --git a/src/components/AttachmentModal.tsx b/src/components/AttachmentModal.tsx
index d1c027378563..54a073e30567 100644
--- a/src/components/AttachmentModal.tsx
+++ b/src/components/AttachmentModal.tsx
@@ -1,4 +1,4 @@
-import Str from 'expensify-common/lib/str';
+import {Str} from 'expensify-common';
import React, {memo, useCallback, useEffect, useMemo, useState} from 'react';
import {Animated, Keyboard, View} from 'react-native';
import {GestureHandlerRootView} from 'react-native-gesture-handler';
diff --git a/src/components/AttachmentPicker/index.native.tsx b/src/components/AttachmentPicker/index.native.tsx
index f6730f4b81d9..154fcf838c86 100644
--- a/src/components/AttachmentPicker/index.native.tsx
+++ b/src/components/AttachmentPicker/index.native.tsx
@@ -1,4 +1,4 @@
-import Str from 'expensify-common/lib/str';
+import {Str} from 'expensify-common';
import React, {useCallback, useMemo, useRef, useState} from 'react';
import {Alert, View} from 'react-native';
import RNFetchBlob from 'react-native-blob-util';
@@ -212,7 +212,7 @@ function AttachmentPicker({type = CONST.ATTACHMENT_PICKER_TYPE.FILE, children, s
* An attachment error dialog when user selected malformed images
*/
const showImageCorruptionAlert = useCallback(() => {
- Alert.alert(translate('attachmentPicker.attachmentError'), translate('attachmentPicker.errorWhileSelectingCorruptedImage'));
+ Alert.alert(translate('attachmentPicker.attachmentError'), translate('attachmentPicker.errorWhileSelectingCorruptedAttachment'));
}, [translate]);
/**
diff --git a/src/components/Attachments/AttachmentView/index.tsx b/src/components/Attachments/AttachmentView/index.tsx
index cee2264894a7..a7409e57f846 100644
--- a/src/components/Attachments/AttachmentView/index.tsx
+++ b/src/components/Attachments/AttachmentView/index.tsx
@@ -1,4 +1,4 @@
-import Str from 'expensify-common/lib/str';
+import {Str} from 'expensify-common';
import React, {memo, useEffect, useState} from 'react';
import type {GestureResponderEvent, StyleProp, ViewStyle} from 'react-native';
import {View} from 'react-native';
diff --git a/src/components/AutoEmailLink.tsx b/src/components/AutoEmailLink.tsx
index e1a9bdd2794b..d64c665a020f 100644
--- a/src/components/AutoEmailLink.tsx
+++ b/src/components/AutoEmailLink.tsx
@@ -1,4 +1,4 @@
-import {CONST} from 'expensify-common/lib/CONST';
+import {CONST as COMMON_CONST} from 'expensify-common';
import React from 'react';
import type {StyleProp, TextStyle} from 'react-native';
import useThemeStyles from '@hooks/useThemeStyles';
@@ -20,8 +20,8 @@ function AutoEmailLink({text, style}: AutoEmailLinkProps) {
const styles = useThemeStyles();
return (
- {text.split(CONST.REG_EXP.EXTRACT_EMAIL).map((str, index) => {
- if (CONST.REG_EXP.EMAIL.test(str)) {
+ {text.split(COMMON_CONST.REG_EXP.EXTRACT_EMAIL).map((str, index) => {
+ if (COMMON_CONST.REG_EXP.EMAIL.test(str)) {
return (
;
diff --git a/src/components/HTMLEngineProvider/HTMLRenderers/AnchorRenderer.tsx b/src/components/HTMLEngineProvider/HTMLRenderers/AnchorRenderer.tsx
index d918007e5750..c6ff10ac927c 100644
--- a/src/components/HTMLEngineProvider/HTMLRenderers/AnchorRenderer.tsx
+++ b/src/components/HTMLEngineProvider/HTMLRenderers/AnchorRenderer.tsx
@@ -1,4 +1,4 @@
-import Str from 'expensify-common/lib/str';
+import {Str} from 'expensify-common';
import React from 'react';
import {TNodeChildrenRenderer} from 'react-native-render-html';
import type {CustomRendererProps, TBlock} from 'react-native-render-html';
diff --git a/src/components/HTMLEngineProvider/HTMLRenderers/MentionUserRenderer.tsx b/src/components/HTMLEngineProvider/HTMLRenderers/MentionUserRenderer.tsx
index 504ddecb492b..a0cb68692e55 100644
--- a/src/components/HTMLEngineProvider/HTMLRenderers/MentionUserRenderer.tsx
+++ b/src/components/HTMLEngineProvider/HTMLRenderers/MentionUserRenderer.tsx
@@ -1,4 +1,4 @@
-import Str from 'expensify-common/lib/str';
+import {Str} from 'expensify-common';
import cloneDeep from 'lodash/cloneDeep';
import isEmpty from 'lodash/isEmpty';
import React from 'react';
diff --git a/src/components/Icon/Expensicons.ts b/src/components/Icon/Expensicons.ts
index a963a0d0908d..5c181bfdb29f 100644
--- a/src/components/Icon/Expensicons.ts
+++ b/src/components/Icon/Expensicons.ts
@@ -139,6 +139,7 @@ import QuestionMark from '@assets/images/question-mark-circle.svg';
import ReceiptPlus from '@assets/images/receipt-plus.svg';
import ReceiptScan from '@assets/images/receipt-scan.svg';
import ReceiptSearch from '@assets/images/receipt-search.svg';
+import ReceiptSlash from '@assets/images/receipt-slash.svg';
import Receipt from '@assets/images/receipt.svg';
import RemoveMembers from '@assets/images/remove-members.svg';
import Rotate from '@assets/images/rotate-image.svg';
@@ -313,6 +314,7 @@ export {
Receipt,
ReceiptPlus,
ReceiptScan,
+ ReceiptSlash,
RemoveMembers,
ReceiptSearch,
Rotate,
diff --git a/src/components/Icon/Illustrations.ts b/src/components/Icon/Illustrations.ts
index 96104932c899..53b8aa8acb72 100644
--- a/src/components/Icon/Illustrations.ts
+++ b/src/components/Icon/Illustrations.ts
@@ -6,6 +6,7 @@ import ConciergeBlue from '@assets/images/product-illustrations/concierge--blue.
import ConciergeExclamation from '@assets/images/product-illustrations/concierge--exclamation.svg';
import CreditCardsBlue from '@assets/images/product-illustrations/credit-cards--blue.svg';
import EmptyStateExpenses from '@assets/images/product-illustrations/emptystate__expenses.svg';
+import EmptyStateTravel from '@assets/images/product-illustrations/emptystate__travel.svg';
import GpsTrackOrange from '@assets/images/product-illustrations/gps-track--orange.svg';
import Hands from '@assets/images/product-illustrations/home-illustration-hands.svg';
import InvoiceOrange from '@assets/images/product-illustrations/invoice--orange.svg';
@@ -182,6 +183,7 @@ export {
Tag,
CarIce,
Lightbulb,
+ EmptyStateTravel,
SubscriptionAnnual,
SubscriptionPPU,
ExpensifyApprovedLogo,
diff --git a/src/components/LHNOptionsList/OptionRowLHN.tsx b/src/components/LHNOptionsList/OptionRowLHN.tsx
index 52d2d0890baf..851e2a9fdd16 100644
--- a/src/components/LHNOptionsList/OptionRowLHN.tsx
+++ b/src/components/LHNOptionsList/OptionRowLHN.tsx
@@ -1,5 +1,5 @@
import {useFocusEffect} from '@react-navigation/native';
-import ExpensiMark from 'expensify-common/lib/ExpensiMark';
+import {ExpensiMark} from 'expensify-common';
import React, {useCallback, useRef, useState} from 'react';
import type {GestureResponderEvent, ViewStyle} from 'react-native';
import {StyleSheet, View} from 'react-native';
diff --git a/src/components/MenuItem.tsx b/src/components/MenuItem.tsx
index 6d42a854601f..554ae9d4a84a 100644
--- a/src/components/MenuItem.tsx
+++ b/src/components/MenuItem.tsx
@@ -1,4 +1,4 @@
-import ExpensiMark from 'expensify-common/lib/ExpensiMark';
+import {ExpensiMark} from 'expensify-common';
import type {ImageContentFit} from 'expo-image';
import type {ReactElement, ReactNode} from 'react';
import React, {forwardRef, useContext, useMemo} from 'react';
diff --git a/src/components/MoneyReportHeader.tsx b/src/components/MoneyReportHeader.tsx
index ec52a6158ad7..d2702fa54a3d 100644
--- a/src/components/MoneyReportHeader.tsx
+++ b/src/components/MoneyReportHeader.tsx
@@ -8,6 +8,7 @@ import useThemeStyles from '@hooks/useThemeStyles';
import useWindowDimensions from '@hooks/useWindowDimensions';
import * as CurrencyUtils from '@libs/CurrencyUtils';
import * as HeaderUtils from '@libs/HeaderUtils';
+import Navigation from '@libs/Navigation/Navigation';
import * as ReportActionsUtils from '@libs/ReportActionsUtils';
import * as ReportUtils from '@libs/ReportUtils';
import * as TransactionUtils from '@libs/TransactionUtils';
@@ -20,14 +21,17 @@ import ROUTES from '@src/ROUTES';
import type * as OnyxTypes from '@src/types/onyx';
import type {PaymentMethodType} from '@src/types/onyx/OriginalMessage';
import {isEmptyObject} from '@src/types/utils/EmptyObject';
+import type IconAsset from '@src/types/utils/IconAsset';
import Button from './Button';
import ConfirmModal from './ConfirmModal';
import HeaderWithBackButton from './HeaderWithBackButton';
import Icon from './Icon';
import * as Expensicons from './Icon/Expensicons';
import MoneyReportHeaderStatusBar from './MoneyReportHeaderStatusBar';
+import type {MoneyRequestHeaderStatusBarProps} from './MoneyRequestHeaderStatusBar';
import MoneyRequestHeaderStatusBar from './MoneyRequestHeaderStatusBar';
import ProcessMoneyReportHoldMenu from './ProcessMoneyReportHoldMenu';
+import ProcessMoneyRequestHoldMenu from './ProcessMoneyRequestHoldMenu';
import SettlementButton from './SettlementButton';
type MoneyReportHeaderProps = {
@@ -56,25 +60,35 @@ function MoneyReportHeader({policy, report: moneyRequestReport, transactionThrea
const [nextStep] = useOnyx(`${ONYXKEYS.COLLECTION.NEXT_STEP}${moneyRequestReport.reportID}`);
const [transactionThreadReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${transactionThreadReportID}`);
const [session] = useOnyx(ONYXKEYS.SESSION);
+ const requestParentReportAction = useMemo(() => {
+ if (!reportActions || !transactionThreadReport?.parentReportActionID) {
+ return null;
+ }
+ return reportActions.find((action) => action.reportActionID === transactionThreadReport.parentReportActionID) as OnyxTypes.ReportAction & OnyxTypes.OriginalMessageIOU;
+ }, [reportActions, transactionThreadReport?.parentReportActionID]);
+ const [transactions] = useOnyx(ONYXKEYS.COLLECTION.TRANSACTION);
+ const [shownHoldUseExplanation] = useOnyx(ONYXKEYS.NVP_HOLD_USE_EXPLAINED, {initWithStoredValues: false});
+ const transaction = transactions?.[`${ONYXKEYS.COLLECTION.TRANSACTION}${requestParentReportAction?.originalMessage?.IOUTransactionID ?? 0}`] ?? null;
const styles = useThemeStyles();
const theme = useTheme();
const [isDeleteRequestModalVisible, setIsDeleteRequestModalVisible] = useState(false);
+ const [shouldShowHoldMenu, setShouldShowHoldMenu] = useState(false);
const {translate} = useLocalize();
const {windowWidth} = useWindowDimensions();
const {reimbursableSpend} = ReportUtils.getMoneyRequestSpendBreakdown(moneyRequestReport);
const isSettled = ReportUtils.isSettled(moneyRequestReport.reportID);
- const requestParentReportAction = useMemo(() => {
- if (!reportActions || !transactionThreadReport?.parentReportActionID) {
- return null;
- }
- return reportActions.find((action) => action.reportActionID === transactionThreadReport.parentReportActionID);
- }, [reportActions, transactionThreadReport?.parentReportActionID]);
+ const isApproved = ReportUtils.isReportApproved(moneyRequestReport);
+ const isOnHold = TransactionUtils.isOnHold(transaction);
+ const isScanning = TransactionUtils.hasReceipt(transaction) && TransactionUtils.isReceiptBeingScanned(transaction);
const isDeletedParentAction = ReportActionsUtils.isDeletedAction(requestParentReportAction as OnyxTypes.ReportAction);
+ const canHoldOrUnholdRequest = !isEmptyObject(transaction) && !isSettled && !isApproved && !isDeletedParentAction;
// Only the requestor can delete the request, admins can only edit it.
const isActionOwner =
typeof requestParentReportAction?.actorAccountID === 'number' && typeof session?.accountID === 'number' && requestParentReportAction.actorAccountID === session?.accountID;
+ const isPolicyAdmin = policy?.role === CONST.POLICY.ROLE.ADMIN;
+ const isApprover = ReportUtils.isMoneyRequestReport(moneyRequestReport) && moneyRequestReport?.managerID !== null && session?.accountID === moneyRequestReport?.managerID;
const canDeleteRequest =
isActionOwner && (ReportUtils.canAddOrDeleteTransactions(moneyRequestReport) || ReportUtils.isTrackExpenseReport(transactionThreadReport)) && !isDeletedParentAction;
const [isHoldMenuVisible, setIsHoldMenuVisible] = useState(false);
@@ -86,8 +100,8 @@ function MoneyReportHeader({policy, report: moneyRequestReport, transactionThrea
const isDraft = ReportUtils.isOpenExpenseReport(moneyRequestReport);
const [isConfirmModalVisible, setIsConfirmModalVisible] = useState(false);
- const hasScanningReceipt = ReportUtils.getTransactionsWithReceipts(moneyRequestReport?.reportID).some((transaction) => TransactionUtils.isReceiptBeingScanned(transaction));
- const transactionIDs = TransactionUtils.getAllReportTransactions(moneyRequestReport?.reportID).map((transaction) => transaction.transactionID);
+ const hasScanningReceipt = ReportUtils.getTransactionsWithReceipts(moneyRequestReport?.reportID).some((t) => TransactionUtils.isReceiptBeingScanned(t));
+ const transactionIDs = TransactionUtils.getAllReportTransactions(moneyRequestReport?.reportID).map((t) => t.transactionID);
const allHavePendingRTERViolation = TransactionUtils.allHavePendingRTERViolation(transactionIDs);
const cancelPayment = useCallback(() => {
@@ -106,18 +120,20 @@ function MoneyReportHeader({policy, report: moneyRequestReport, transactionThrea
const shouldShowSettlementButton = (shouldShowPayButton || shouldShowApproveButton) && !allHavePendingRTERViolation;
- const shouldShowSubmitButton = isDraft && reimbursableSpend !== 0 && !allHavePendingRTERViolation;
+ // allTransactions in TransactionUtils might have stale data
+ const hasOnlyHeldExpenses = ReportUtils.hasOnlyHeldExpenses(moneyRequestReport.reportID, transactions);
+ const shouldShowSubmitButton = isDraft && reimbursableSpend !== 0 && !allHavePendingRTERViolation && !hasOnlyHeldExpenses;
const shouldDisableSubmitButton = shouldShowSubmitButton && !ReportUtils.isAllowedToSubmitDraftExpenseReport(moneyRequestReport);
- const shouldShowMarkAsCashButton = isDraft && allHavePendingRTERViolation;
+ const shouldShowMarkAsCashButton = isDraft && allHavePendingRTERViolation && !hasOnlyHeldExpenses;
const isFromPaidPolicy = policyType === CONST.POLICY.TYPE.TEAM || policyType === CONST.POLICY.TYPE.CORPORATE;
- const shouldShowNextStep =
- !ReportUtils.isClosedExpenseReportWithNoExpenses(moneyRequestReport) && isFromPaidPolicy && !!nextStep?.message?.length && !allHavePendingRTERViolation && !hasScanningReceipt;
+ const shouldShowStatusBar = allHavePendingRTERViolation || hasOnlyHeldExpenses || hasScanningReceipt;
+ const shouldShowNextStep = !ReportUtils.isClosedExpenseReportWithNoExpenses(moneyRequestReport) && isFromPaidPolicy && !!nextStep?.message?.length && !shouldShowStatusBar;
const shouldShowAnyButton = shouldShowSettlementButton || shouldShowApproveButton || shouldShowSubmitButton || shouldShowNextStep || allHavePendingRTERViolation;
const bankAccountRoute = ReportUtils.getBankAccountRoute(chatReport);
const formattedAmount = CurrencyUtils.convertToDisplayString(reimbursableSpend, moneyRequestReport.currency);
const [nonHeldAmount, fullAmount] = ReportUtils.getNonHeldAndFullAmount(moneyRequestReport, policy);
const displayedAmount = ReportUtils.hasHeldExpenses(moneyRequestReport.reportID) && canAllowSettlement ? nonHeldAmount : formattedAmount;
- const isMoreContentShown = shouldShowNextStep || hasScanningReceipt || (shouldShowAnyButton && shouldUseNarrowLayout);
+ const isMoreContentShown = shouldShowNextStep || shouldShowStatusBar || (shouldShowAnyButton && shouldUseNarrowLayout);
const confirmPayment = (type?: PaymentMethodType | undefined) => {
if (!type || !chatReport) {
@@ -166,6 +182,43 @@ function MoneyReportHeader({policy, report: moneyRequestReport, transactionThrea
TransactionActions.markAsCash(iouTransactionID, reportID);
}, [requestParentReportAction, transactionThreadReport?.reportID]);
+ const changeMoneyRequestStatus = () => {
+ if (!transactionThreadReport) {
+ return;
+ }
+ const transactionID = requestParentReportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.IOU ? requestParentReportAction.originalMessage?.IOUTransactionID ?? '' : '';
+
+ if (isOnHold) {
+ IOU.unholdRequest(transactionID, transactionThreadReport.reportID);
+ } else {
+ const activeRoute = encodeURIComponent(Navigation.getActiveRouteWithoutParams());
+ Navigation.navigate(ROUTES.MONEY_REQUEST_HOLD_REASON.getRoute(policy?.type ?? CONST.POLICY.TYPE.PERSONAL, transactionID, transactionThreadReport.reportID, activeRoute));
+ }
+ };
+
+ const getStatusIcon: (src: IconAsset) => React.ReactNode = (src) => (
+
+ );
+
+ const getStatusBarProps: () => MoneyRequestHeaderStatusBarProps | undefined = () => {
+ if (hasOnlyHeldExpenses) {
+ return {title: translate('iou.hold'), description: translate('iou.expensesOnHold'), danger: true};
+ }
+ if (allHavePendingRTERViolation) {
+ return {title: getStatusIcon(Expensicons.Hourglass), description: translate('iou.pendingMatchWithCreditCardDescription')};
+ }
+ if (hasScanningReceipt) {
+ return {title: getStatusIcon(Expensicons.ReceiptScan), description: translate('iou.receiptScanInProgressDescription')};
+ }
+ };
+
+ const statusBarProps = getStatusBarProps();
+
// The submit button should be success green colour only if the user is submitter and the policy does not have Scheduled Submit turned on
const isWaitingForSubmissionFromCurrentUser = useMemo(
() => chatReport?.isOwnPolicyExpenseChat && !policy?.harvesting?.enabled,
@@ -173,6 +226,49 @@ function MoneyReportHeader({policy, report: moneyRequestReport, transactionThrea
);
const threeDotsMenuItems = [HeaderUtils.getPinMenuItem(moneyRequestReport)];
+ if (canHoldOrUnholdRequest) {
+ const isRequestIOU = ReportUtils.isIOUReport(chatReport);
+ const isHoldCreator = ReportUtils.isHoldCreator(transaction, moneyRequestReport?.reportID) && isRequestIOU;
+ const isTrackExpenseReport = ReportUtils.isTrackExpenseReport(moneyRequestReport);
+ const canModifyStatus = !isTrackExpenseReport && (isPolicyAdmin || isActionOwner || isApprover);
+ if (isOnHold && (isHoldCreator || (!isRequestIOU && canModifyStatus))) {
+ threeDotsMenuItems.push({
+ icon: Expensicons.Stopwatch,
+ text: translate('iou.unholdExpense'),
+ onSelected: () => changeMoneyRequestStatus(),
+ });
+ }
+ if (!isOnHold && (isRequestIOU || canModifyStatus) && !isScanning) {
+ threeDotsMenuItems.push({
+ icon: Expensicons.Stopwatch,
+ text: translate('iou.hold'),
+ onSelected: () => changeMoneyRequestStatus(),
+ });
+ }
+ }
+
+ useEffect(() => {
+ setShouldShowHoldMenu(isOnHold && !shownHoldUseExplanation);
+ }, [isOnHold, shownHoldUseExplanation]);
+
+ useEffect(() => {
+ if (!shouldShowHoldMenu) {
+ return;
+ }
+
+ if (shouldUseNarrowLayout) {
+ if (Navigation.getActiveRoute().slice(1) === ROUTES.PROCESS_MONEY_REQUEST_HOLD) {
+ Navigation.goBack();
+ }
+ } else {
+ Navigation.navigate(ROUTES.PROCESS_MONEY_REQUEST_HOLD);
+ }
+ }, [shouldUseNarrowLayout, shouldShowHoldMenu]);
+
+ const handleHoldRequestClose = () => {
+ IOU.setShownHoldUseExplanation();
+ };
+
if (isPayer && isSettled && ReportUtils.isExpenseReport(moneyRequestReport)) {
threeDotsMenuItems.push({
icon: Expensicons.Trashcan,
@@ -208,8 +304,8 @@ function MoneyReportHeader({policy, report: moneyRequestReport, transactionThrea
policy={policy}
shouldShowBackButton={shouldUseNarrowLayout}
onBackButtonPress={onBackButtonPress}
- // Shows border if no buttons or next steps are showing below the header
- shouldShowBorderBottom={!isMoreContentShown && !allHavePendingRTERViolation}
+ // Shows border if no buttons or banners are showing below the header
+ shouldShowBorderBottom={!isMoreContentShown}
shouldShowThreeDotsButton
threeDotsMenuItems={threeDotsMenuItems}
threeDotsAnchorPosition={styles.threeDotsPopoverOffsetNoCloseButton(windowWidth)}
@@ -229,7 +325,7 @@ function MoneyReportHeader({policy, report: moneyRequestReport, transactionThrea
shouldShowApproveButton={shouldShowApproveButton}
shouldDisableApproveButton={shouldDisableApproveButton}
style={[styles.pv2]}
- formattedAmount={!ReportUtils.hasOnlyHeldExpenses(moneyRequestReport.reportID) ? displayedAmount : ''}
+ formattedAmount={!hasOnlyHeldExpenses ? displayedAmount : ''}
isDisabled={!canAllowSettlement}
/>
@@ -258,88 +354,55 @@ function MoneyReportHeader({policy, report: moneyRequestReport, transactionThrea
)}
- {allHavePendingRTERViolation && (
-
- {shouldShowMarkAsCashButton && shouldUseNarrowLayout && (
-
-
-
- )}
-
- }
- description={translate('iou.pendingMatchWithCreditCardDescription')}
- shouldShowBorderBottom
- />
-
- )}
-
+
{shouldShowSettlementButton && shouldUseNarrowLayout && (
-
-
-
+
)}
{shouldShowSubmitButton && shouldUseNarrowLayout && (
-
-
+
{isHoldMenuVisible && requestType !== undefined && (
+ {shouldUseNarrowLayout && shouldShowHoldMenu && (
+
+ )}
);
}
diff --git a/src/components/MoneyRequestConfirmationList.tsx b/src/components/MoneyRequestConfirmationList.tsx
index a82cbbe61668..ba5c65a4fbd6 100755
--- a/src/components/MoneyRequestConfirmationList.tsx
+++ b/src/components/MoneyRequestConfirmationList.tsx
@@ -1,6 +1,6 @@
import {useIsFocused} from '@react-navigation/native';
import {format} from 'date-fns';
-import Str from 'expensify-common/lib/str';
+import {Str} from 'expensify-common';
import React, {useCallback, useEffect, useMemo, useReducer, useState} from 'react';
import type {TextStyle} from 'react-native';
import {View} from 'react-native';
@@ -328,6 +328,7 @@ function MoneyRequestConfirmationList({
const [didConfirmSplit, setDidConfirmSplit] = useState(false);
const [isAttachmentInvalid, setIsAttachmentInvalid] = useState(false);
+ const [invalidAttachmentPromt, setInvalidAttachmentPromt] = useState(translate('attachmentPicker.protectedPDFNotSupported'));
const navigateBack = useCallback(
() => Navigation.goBack(ROUTES.MONEY_REQUEST_CREATE_TAB_SCAN.getRoute(CONST.IOU.ACTION.CREATE, iouType, transactionID, reportID)),
@@ -343,7 +344,7 @@ function MoneyRequestConfirmationList({
}, [isEditingSplitBill, hasSmartScanFailed, transaction, didConfirmSplit]);
const isMerchantEmpty = useMemo(() => !iouMerchant || TransactionUtils.isMerchantMissing(transaction), [transaction, iouMerchant]);
- const isMerchantRequired = isPolicyExpenseChat && (!isScanRequest || isEditingSplitBill) && shouldShowMerchant;
+ const isMerchantRequired = (isPolicyExpenseChat || isTypeInvoice) && (!isScanRequest || isEditingSplitBill) && shouldShowMerchant;
const shouldDisplayMerchantError = isMerchantRequired && (shouldDisplayFieldError || formError === 'iou.error.invalidMerchant') && isMerchantEmpty;
const isCategoryRequired = !!policy?.requiresCategory;
@@ -1100,7 +1101,14 @@ function MoneyRequestConfirmationList({
previewSourceURL={resolvedReceiptImage as string}
// We don't support scanning password protected PDF receipt
enabled={!isAttachmentInvalid}
- onPassword={() => setIsAttachmentInvalid(true)}
+ onPassword={() => {
+ setIsAttachmentInvalid(true);
+ setInvalidAttachmentPromt(translate('attachmentPicker.protectedPDFNotSupported'));
+ }}
+ onLoadError={() => {
+ setInvalidAttachmentPromt(translate('attachmentPicker.errorWhileSelectingCorruptedAttachment'));
+ setIsAttachmentInvalid(true);
+ }}
/>
) : (
{shouldShowAllFields && supplementaryFields}
@@ -1226,6 +1235,7 @@ function MoneyRequestConfirmationList({
transaction,
transactionID,
translate,
+ invalidAttachmentPromt,
],
);
diff --git a/src/components/MoneyRequestHeader.tsx b/src/components/MoneyRequestHeader.tsx
index abd70753b461..1d2579169c22 100644
--- a/src/components/MoneyRequestHeader.tsx
+++ b/src/components/MoneyRequestHeader.tsx
@@ -120,21 +120,17 @@ function MoneyRequestHeader({report, parentReportAction, policy, shouldUseNarrow
const getStatusBarProps: () => MoneyRequestHeaderStatusBarProps | undefined = () => {
if (isOnHold) {
- return {title: translate('iou.hold'), description: translate('iou.expenseOnHold'), danger: true, shouldShowBorderBottom: true};
+ return {title: translate('iou.hold'), description: translate('iou.expenseOnHold'), danger: true};
}
if (TransactionUtils.isExpensifyCardTransaction(transaction) && TransactionUtils.isPending(transaction)) {
- return {title: getStatusIcon(Expensicons.CreditCardHourglass), description: translate('iou.transactionPendingDescription'), shouldShowBorderBottom: true};
+ return {title: getStatusIcon(Expensicons.CreditCardHourglass), description: translate('iou.transactionPendingDescription')};
}
if (TransactionUtils.hasPendingRTERViolation(TransactionUtils.getTransactionViolations(transaction?.transactionID ?? '', transactionViolations))) {
- return {
- title: getStatusIcon(Expensicons.Hourglass),
- description: translate('iou.pendingMatchWithCreditCardDescription'),
- shouldShowBorderBottom: true,
- };
+ return {title: getStatusIcon(Expensicons.Hourglass), description: translate('iou.pendingMatchWithCreditCardDescription')};
}
if (isScanning) {
- return {title: getStatusIcon(Expensicons.ReceiptScan), description: translate('iou.receiptScanInProgressDescription'), shouldShowBorderBottom: true};
+ return {title: getStatusIcon(Expensicons.ReceiptScan), description: translate('iou.receiptScanInProgressDescription')};
}
};
@@ -241,12 +237,13 @@ function MoneyRequestHeader({report, parentReportAction, policy, shouldUseNarrow
)}
{statusBarProps && (
-
+
+
+
)}
+
{typeof title === 'string' ? (
+
+
+ );
+}
+
+export default PDFThumbnailError;
diff --git a/src/components/PDFThumbnail/index.native.tsx b/src/components/PDFThumbnail/index.native.tsx
index 0232dba99f05..27d41ede3263 100644
--- a/src/components/PDFThumbnail/index.native.tsx
+++ b/src/components/PDFThumbnail/index.native.tsx
@@ -1,19 +1,21 @@
-import React from 'react';
+import React, {useState} from 'react';
import {View} from 'react-native';
import Pdf from 'react-native-pdf';
import FullScreenLoadingIndicator from '@components/FullscreenLoadingIndicator';
import useThemeStyles from '@hooks/useThemeStyles';
import addEncryptedAuthTokenToURL from '@libs/addEncryptedAuthTokenToURL';
+import PDFThumbnailError from './PDFThumbnailError';
import type PDFThumbnailProps from './types';
-function PDFThumbnail({previewSourceURL, style, isAuthTokenRequired = false, enabled = true, onPassword}: PDFThumbnailProps) {
+function PDFThumbnail({previewSourceURL, style, isAuthTokenRequired = false, enabled = true, onPassword, onLoadError}: PDFThumbnailProps) {
const styles = useThemeStyles();
const sizeStyles = [styles.w100, styles.h100];
+ const [failedToLoad, setFailedToLoad] = useState(false);
return (
-
- {enabled && (
+
+ {enabled && !failedToLoad && (
{
- if (!('message' in error && typeof error.message === 'string' && error.message.match(/password/i))) {
- return;
+ if (onLoadError) {
+ onLoadError();
}
- if (!onPassword) {
+ if ('message' in error && typeof error.message === 'string' && error.message.match(/password/i) && onPassword) {
+ onPassword();
return;
}
- onPassword();
+ setFailedToLoad(true);
}}
/>
)}
+ {failedToLoad && }
);
diff --git a/src/components/PDFThumbnail/index.tsx b/src/components/PDFThumbnail/index.tsx
index ce631f3b611f..8e79c027cf03 100644
--- a/src/components/PDFThumbnail/index.tsx
+++ b/src/components/PDFThumbnail/index.tsx
@@ -1,18 +1,20 @@
import pdfWorkerSource from 'pdfjs-dist/legacy/build/pdf.worker';
-import React, {useMemo} from 'react';
+import React, {useMemo, useState} from 'react';
import {View} from 'react-native';
import {Document, pdfjs, Thumbnail} from 'react-pdf';
import FullScreenLoadingIndicator from '@components/FullscreenLoadingIndicator';
import useThemeStyles from '@hooks/useThemeStyles';
import addEncryptedAuthTokenToURL from '@libs/addEncryptedAuthTokenToURL';
+import PDFThumbnailError from './PDFThumbnailError';
import type PDFThumbnailProps from './types';
if (!pdfjs.GlobalWorkerOptions.workerSrc) {
pdfjs.GlobalWorkerOptions.workerSrc = URL.createObjectURL(new Blob([pdfWorkerSource], {type: 'text/javascript'}));
}
-function PDFThumbnail({previewSourceURL, style, isAuthTokenRequired = false, enabled = true, onPassword}: PDFThumbnailProps) {
+function PDFThumbnail({previewSourceURL, style, isAuthTokenRequired = false, enabled = true, onPassword, onLoadError}: PDFThumbnailProps) {
const styles = useThemeStyles();
+ const [failedToLoad, setFailedToLoad] = useState(false);
const thumbnail = useMemo(
() => (
@@ -25,18 +27,31 @@ function PDFThumbnail({previewSourceURL, style, isAuthTokenRequired = false, ena
}}
externalLinkTarget="_blank"
onPassword={onPassword}
+ onLoad={() => {
+ setFailedToLoad(false);
+ }}
+ onLoadError={() => {
+ if (onLoadError) {
+ onLoadError();
+ }
+ setFailedToLoad(true);
+ }}
+ error={() => null}
>
),
- [isAuthTokenRequired, previewSourceURL, onPassword],
+ [isAuthTokenRequired, previewSourceURL, onPassword, onLoadError],
);
return (
-
- {enabled && thumbnail}
+
+
+ {enabled && !failedToLoad && thumbnail}
+ {failedToLoad && }
+
);
}
diff --git a/src/components/PDFThumbnail/types.ts b/src/components/PDFThumbnail/types.ts
index 11253e462aca..349669ecc33e 100644
--- a/src/components/PDFThumbnail/types.ts
+++ b/src/components/PDFThumbnail/types.ts
@@ -15,6 +15,9 @@ type PDFThumbnailProps = {
/** Callback to call if PDF is password protected */
onPassword?: () => void;
+
+ /** Callback to call if PDF can't be loaded(corrupted) */
+ onLoadError?: () => void;
};
export default PDFThumbnailProps;
diff --git a/src/components/ReportActionItem/MoneyReportView.tsx b/src/components/ReportActionItem/MoneyReportView.tsx
index 81d59d750284..673801f164d2 100644
--- a/src/components/ReportActionItem/MoneyReportView.tsx
+++ b/src/components/ReportActionItem/MoneyReportView.tsx
@@ -1,4 +1,4 @@
-import Str from 'expensify-common/lib/str';
+import {Str} from 'expensify-common';
import React, {useMemo} from 'react';
import type {StyleProp, TextStyle} from 'react-native';
import {View} from 'react-native';
diff --git a/src/components/ReportActionItem/ReportActionItemImage.tsx b/src/components/ReportActionItem/ReportActionItemImage.tsx
index 4e7f123a2fb2..0dbf0e6beadf 100644
--- a/src/components/ReportActionItem/ReportActionItemImage.tsx
+++ b/src/components/ReportActionItem/ReportActionItemImage.tsx
@@ -1,5 +1,5 @@
/* eslint-disable react/jsx-props-no-spreading */
-import Str from 'expensify-common/lib/str';
+import {Str} from 'expensify-common';
import React from 'react';
import type {ViewStyle} from 'react-native';
import type {OnyxEntry} from 'react-native-onyx';
diff --git a/src/components/ReportActionItem/TaskPreview.tsx b/src/components/ReportActionItem/TaskPreview.tsx
index 8e8b3b930be7..553051e07104 100644
--- a/src/components/ReportActionItem/TaskPreview.tsx
+++ b/src/components/ReportActionItem/TaskPreview.tsx
@@ -1,4 +1,4 @@
-import Str from 'expensify-common/lib/str';
+import {Str} from 'expensify-common';
import React from 'react';
import {View} from 'react-native';
import {withOnyx} from 'react-native-onyx';
diff --git a/src/components/Search.tsx b/src/components/Search.tsx
index fcb915379181..28c3ae5fdc4b 100644
--- a/src/components/Search.tsx
+++ b/src/components/Search.tsx
@@ -32,6 +32,8 @@ type SearchProps = {
sortOrder?: SortOrder;
};
+const sortableSearchTabs: SearchQuery[] = [CONST.TAB_SEARCH.ALL];
+
function isReportListItemType(item: TransactionListItemType | ReportListItemType): item is ReportListItemType {
const reportListItem = item as ReportListItemType;
return reportListItem.transactions !== undefined;
@@ -100,6 +102,7 @@ function Search({query, policyIDs, sortBy, sortOrder}: SearchProps) {
const ListItem = SearchUtils.getListItem(type);
const data = SearchUtils.getSections(searchResults?.data ?? {}, type);
+ const sortedData = SearchUtils.getSortedSections(type, data, sortBy, sortOrder);
const onSortPress = (column: SearchColumnType, order: SortOrder) => {
navigation.setParams({
@@ -108,7 +111,7 @@ function Search({query, policyIDs, sortBy, sortOrder}: SearchProps) {
});
};
- const sortedData = SearchUtils.getSortedSections(type, data, sortBy, sortOrder);
+ const isSortingAllowed = sortableSearchTabs.includes(query);
return (
@@ -117,6 +120,7 @@ function Search({query, policyIDs, sortBy, sortOrder}: SearchProps) {
data={searchResults?.data}
onSortPress={onSortPress}
sortOrder={sortOrder}
+ isSortingAllowed={isSortingAllowed}
sortBy={sortBy}
/>
}
@@ -135,7 +139,6 @@ function Search({query, policyIDs, sortBy, sortOrder}: SearchProps) {
sections={[{data: sortedData, isDisabled: false}]}
onSelectRow={(item) => {
const reportID = isReportListItemType(item) ? item.reportID : item.transactionThreadReportID;
-
openReport(reportID);
}}
shouldDebounceRowSelect
diff --git a/src/components/Section/IconSection.tsx b/src/components/Section/IconSection.tsx
index cc42c6b7ace5..df5392027df4 100644
--- a/src/components/Section/IconSection.tsx
+++ b/src/components/Section/IconSection.tsx
@@ -3,14 +3,20 @@ import type {StyleProp, ViewStyle} from 'react-native';
import {View} from 'react-native';
import Icon from '@components/Icon';
import useThemeStyles from '@hooks/useThemeStyles';
+import variables from '@styles/variables';
import type IconAsset from '@src/types/utils/IconAsset';
type IconSectionProps = {
icon?: IconAsset;
iconContainerStyles?: StyleProp;
+ /** The width of the icon. */
+ width?: number;
+
+ /** The height of the icon. */
+ height?: number;
};
-function IconSection({icon, iconContainerStyles}: IconSectionProps) {
+function IconSection({icon, iconContainerStyles, width = variables.menuIconSize, height = variables.menuIconSize}: IconSectionProps) {
const styles = useThemeStyles();
return (
@@ -18,8 +24,8 @@ function IconSection({icon, iconContainerStyles}: IconSectionProps) {
{!!icon && (
)}
diff --git a/src/components/Section/index.tsx b/src/components/Section/index.tsx
index 9d6e6cbbd41f..e7cadbe73b82 100644
--- a/src/components/Section/index.tsx
+++ b/src/components/Section/index.tsx
@@ -1,8 +1,9 @@
-import React from 'react';
import type {ReactNode} from 'react';
+import React from 'react';
import type {StyleProp, TextStyle, ViewStyle} from 'react-native';
import {View} from 'react-native';
import type {ValueOf} from 'type-fest';
+import ImageSVG from '@components/ImageSVG';
import Lottie from '@components/Lottie';
import type DotLottieAnimation from '@components/LottieAnimations/types';
import type {MenuItemWithLink} from '@components/MenuItemList';
@@ -22,12 +23,12 @@ const CARD_LAYOUT = {
ICON_ON_RIGHT: 'iconOnRight',
} as const;
-type SectionProps = ChildrenProps & {
+type SectionProps = Partial & {
/** An array of props that are passed to individual MenuItem components */
menuItems?: MenuItemWithLink[];
/** The text to display in the title of the section */
- title: string;
+ title?: string;
/** The text to display in the subtitle of the section */
subtitle?: string;
@@ -59,8 +60,8 @@ type SectionProps = ChildrenProps & {
/** Whether the section is in the central pane of the layout */
isCentralPane?: boolean;
- /** The illustration to display in the header. Can be a JSON object representing a Lottie animation. */
- illustration?: DotLottieAnimation;
+ /** The illustration to display in the header. Can be an image or a JSON object representing a Lottie animation. */
+ illustration?: DotLottieAnimation | IconAsset;
/** The background color to apply in the upper half of the screen. */
illustrationBackgroundColor?: string;
@@ -76,8 +77,25 @@ type SectionProps = ChildrenProps & {
/** The component to display in the title of the section */
renderSubtitle?: () => ReactNode;
+
+ /** The component to display custom title */
+ renderTitle?: () => ReactNode;
+
+ /** The width of the icon. */
+ iconWidth?: number;
+
+ /** The height of the icon. */
+ iconHeight?: number;
};
+function isIllustrationLottieAnimation(illustration: DotLottieAnimation | IconAsset | undefined): illustration is DotLottieAnimation {
+ if (typeof illustration === 'number' || !illustration) {
+ return false;
+ }
+
+ return 'file' in illustration && 'w' in illustration && 'h' in illustration;
+}
+
function Section({
children,
childrenStyles,
@@ -90,6 +108,7 @@ function Section({
subtitleStyles,
subtitleMuted = false,
title,
+ renderTitle,
titleStyles,
isCentralPane = false,
illustration,
@@ -97,6 +116,8 @@ function Section({
illustrationStyle,
contentPaddingOnLargeScreens,
overlayContent,
+ iconWidth,
+ iconHeight,
renderSubtitle,
}: SectionProps) {
const styles = useThemeStyles();
@@ -104,12 +125,18 @@ function Section({
const StyleUtils = useStyleUtils();
const {shouldUseNarrowLayout} = useResponsiveLayout();
- const illustrationContainerStyle: StyleProp = StyleUtils.getBackgroundColorStyle(illustrationBackgroundColor ?? illustration?.backgroundColor ?? theme.appBG);
+ const isLottie = isIllustrationLottieAnimation(illustration);
+
+ const lottieIllustration = isLottie ? illustration : undefined;
+
+ const illustrationContainerStyle: StyleProp = StyleUtils.getBackgroundColorStyle(illustrationBackgroundColor ?? lottieIllustration?.backgroundColor ?? theme.appBG);
return (
{cardLayout === CARD_LAYOUT.ICON_ON_TOP && (
@@ -117,13 +144,20 @@ function Section({
{!!illustration && (
-
+ {isLottie ? (
+
+ ) : (
+
+ )}
{overlayContent?.()}
@@ -132,15 +166,17 @@ function Section({
{cardLayout === CARD_LAYOUT.ICON_ON_LEFT && (
)}
-
- {title}
-
+ {renderTitle ? renderTitle() : {title}}
{cardLayout === CARD_LAYOUT.ICON_ON_RIGHT && (
diff --git a/src/components/SelectionList/InviteMemberListItem.tsx b/src/components/SelectionList/InviteMemberListItem.tsx
index 69774e24b970..13b0014efb2d 100644
--- a/src/components/SelectionList/InviteMemberListItem.tsx
+++ b/src/components/SelectionList/InviteMemberListItem.tsx
@@ -1,4 +1,4 @@
-import Str from 'expensify-common/lib/str';
+import {Str} from 'expensify-common';
import React, {useCallback} from 'react';
import {View} from 'react-native';
import MultipleAvatars from '@components/MultipleAvatars';
diff --git a/src/components/SelectionList/Search/ReportListItem.tsx b/src/components/SelectionList/Search/ReportListItem.tsx
index cfd047ffd115..10e25f3d871c 100644
--- a/src/components/SelectionList/Search/ReportListItem.tsx
+++ b/src/components/SelectionList/Search/ReportListItem.tsx
@@ -18,6 +18,48 @@ import ExpenseItemHeaderNarrow from './ExpenseItemHeaderNarrow';
import TransactionListItem from './TransactionListItem';
import TransactionListItemRow from './TransactionListItemRow';
+type CellProps = {
+ // eslint-disable-next-line react/no-unused-prop-types
+ showTooltip: boolean;
+ // eslint-disable-next-line react/no-unused-prop-types
+ isLargeScreenWidth: boolean;
+};
+
+type ReportCellProps = {
+ reportItem: ReportListItemType;
+} & CellProps;
+
+type ActionCellProps = {
+ onButtonPress: () => void;
+} & CellProps;
+
+function TotalCell({showTooltip, isLargeScreenWidth, reportItem}: ReportCellProps) {
+ const styles = useThemeStyles();
+
+ return (
+
+ );
+}
+
+function ActionCell({onButtonPress}: ActionCellProps) {
+ const {translate} = useLocalize();
+ const styles = useThemeStyles();
+
+ return (
+
+ );
+}
+
function ReportListItem({
item,
isFocused,
@@ -45,28 +87,10 @@ function ReportListItem({
const openReportInRHP = (transactionItem: TransactionListItemType) => {
const searchParams = getSearchParams();
- const currentQuery = searchParams && `query` in searchParams ? (searchParams?.query as string) : CONST.TAB_SEARCH.ALL;
+ const currentQuery = searchParams?.query ?? CONST.TAB_SEARCH.ALL;
Navigation.navigate(ROUTES.SEARCH_REPORT.getRoute(currentQuery, transactionItem.transactionThreadReportID));
};
- const totalCell = (
-
- );
-
- const actionCell = (
-
- );
-
if (!reportItem?.reportName && reportItem.transactions.length > 1) {
return null;
}
@@ -138,13 +162,25 @@ function ReportListItem({
{`${reportItem.transactions.length} ${translate('search.groupedExpenses')}`}
- {totalCell}
+
+
+
{isLargeScreenWidth && (
<>
{/** We add an empty view with type style to align the total with the table header */}
- {actionCell}
+
+
+
>
)}
diff --git a/src/components/SelectionList/Search/TransactionListItemRow.tsx b/src/components/SelectionList/Search/TransactionListItemRow.tsx
index 7a1fa8709c49..02ac0bcd700d 100644
--- a/src/components/SelectionList/Search/TransactionListItemRow.tsx
+++ b/src/components/SelectionList/Search/TransactionListItemRow.tsx
@@ -353,7 +353,7 @@ function TransactionListItemRow({
)}
-
+
boolean;
};
@@ -23,7 +23,7 @@ const SearchColumns: SearchColumnConfig[] = [
columnName: CONST.SEARCH_TABLE_COLUMNS.RECEIPT,
translationKey: 'common.receipt',
shouldShow: () => true,
- isSortable: false,
+ isColumnSortable: false,
},
{
columnName: CONST.SEARCH_TABLE_COLUMNS.DATE,
@@ -66,7 +66,7 @@ const SearchColumns: SearchColumnConfig[] = [
shouldShow: (data: OnyxTypes.SearchResults['data']) => SearchUtils.getShouldShowColumn(data, CONST.SEARCH_TABLE_COLUMNS.TAX_AMOUNT),
},
{
- columnName: CONST.SEARCH_TABLE_COLUMNS.TOTAL,
+ columnName: CONST.SEARCH_TABLE_COLUMNS.TOTAL_AMOUNT,
translationKey: 'common.total',
shouldShow: () => true,
},
@@ -74,11 +74,13 @@ const SearchColumns: SearchColumnConfig[] = [
columnName: CONST.SEARCH_TABLE_COLUMNS.TYPE,
translationKey: 'common.type',
shouldShow: () => true,
+ isColumnSortable: false,
},
{
columnName: CONST.SEARCH_TABLE_COLUMNS.ACTION,
translationKey: 'common.action',
shouldShow: () => true,
+ isColumnSortable: false,
},
];
@@ -86,10 +88,11 @@ type SearchTableHeaderProps = {
data: OnyxTypes.SearchResults['data'];
sortBy?: SearchColumnType;
sortOrder?: SortOrder;
+ isSortingAllowed: boolean;
onSortPress: (column: SearchColumnType, order: SortOrder) => void;
};
-function SearchTableHeader({data, sortBy, sortOrder, onSortPress}: SearchTableHeaderProps) {
+function SearchTableHeader({data, sortBy, sortOrder, isSortingAllowed, onSortPress}: SearchTableHeaderProps) {
const styles = useThemeStyles();
const StyleUtils = useStyleUtils();
const {isSmallScreenWidth, isMediumScreenWidth} = useWindowDimensions();
@@ -103,9 +106,14 @@ function SearchTableHeader({data, sortBy, sortOrder, onSortPress}: SearchTableHe
return (
- {SearchColumns.map(({columnName, translationKey, shouldShow, isSortable}) => {
+ {SearchColumns.map(({columnName, translationKey, shouldShow, isColumnSortable}) => {
+ if (!shouldShow(data)) {
+ return null;
+ }
+
const isActive = sortBy === columnName;
const textStyle = columnName === CONST.SEARCH_TABLE_COLUMNS.RECEIPT ? StyleUtils.getTextOverflowStyle('clip') : null;
+ const isSortable = isSortingAllowed && isColumnSortable;
return (
onSortPress(columnName, order)}
/>
diff --git a/src/components/SelectionList/SortableHeaderText.tsx b/src/components/SelectionList/SortableHeaderText.tsx
index d84d438b3749..bd5f4873bbbc 100644
--- a/src/components/SelectionList/SortableHeaderText.tsx
+++ b/src/components/SelectionList/SortableHeaderText.tsx
@@ -14,23 +14,34 @@ type SearchTableHeaderColumnProps = {
text: string;
isActive: boolean;
sortOrder: SortOrder;
- shouldShow?: boolean;
isSortable?: boolean;
containerStyle?: StyleProp;
textStyle?: StyleProp;
onPress: (order: SortOrder) => void;
};
-export default function SortableHeaderText({text, sortOrder, isActive, textStyle, containerStyle, shouldShow = true, isSortable = true, onPress}: SearchTableHeaderColumnProps) {
+export default function SortableHeaderText({text, sortOrder, isActive, textStyle, containerStyle, isSortable = true, onPress}: SearchTableHeaderColumnProps) {
const styles = useThemeStyles();
const theme = useTheme();
- if (!shouldShow) {
- return null;
+ if (!isSortable) {
+ return (
+
+
+
+ {text}
+
+
+
+ );
}
const icon = sortOrder === CONST.SORT_ORDER.ASC ? Expensicons.ArrowUpLong : Expensicons.ArrowDownLong;
const displayIcon = isActive;
+ const activeColumnStyle = isSortable && isActive && styles.searchTableHeaderActive;
const nextSortOrder = isActive && sortOrder === CONST.SORT_ORDER.DESC ? CONST.SORT_ORDER.ASC : CONST.SORT_ORDER.DESC;
@@ -46,7 +57,7 @@ export default function SortableHeaderText({text, sortOrder, isActive, textStyle
{text}
diff --git a/src/components/SelectionList/UserListItem.tsx b/src/components/SelectionList/UserListItem.tsx
index 8fe9518c7d90..d07ac03c00f5 100644
--- a/src/components/SelectionList/UserListItem.tsx
+++ b/src/components/SelectionList/UserListItem.tsx
@@ -1,4 +1,4 @@
-import Str from 'expensify-common/lib/str';
+import {Str} from 'expensify-common';
import React, {useCallback} from 'react';
import {View} from 'react-native';
import Icon from '@components/Icon';
diff --git a/src/components/StateSelector.tsx b/src/components/StateSelector.tsx
index 8cae007679ff..67ba80c13ef8 100644
--- a/src/components/StateSelector.tsx
+++ b/src/components/StateSelector.tsx
@@ -1,5 +1,5 @@
import {useIsFocused} from '@react-navigation/native';
-import {CONST as COMMON_CONST} from 'expensify-common/lib/CONST';
+import {CONST as COMMON_CONST} from 'expensify-common';
import React, {useEffect, useRef} from 'react';
import type {ForwardedRef} from 'react';
import type {View} from 'react-native';
diff --git a/src/components/TextInput/BaseTextInput/index.native.tsx b/src/components/TextInput/BaseTextInput/index.native.tsx
index 4f0c7dd71116..809544d2c5ed 100644
--- a/src/components/TextInput/BaseTextInput/index.native.tsx
+++ b/src/components/TextInput/BaseTextInput/index.native.tsx
@@ -1,4 +1,4 @@
-import Str from 'expensify-common/lib/str';
+import {Str} from 'expensify-common';
import type {ForwardedRef} from 'react';
import React, {forwardRef, useCallback, useEffect, useRef, useState} from 'react';
import type {GestureResponderEvent, LayoutChangeEvent, NativeSyntheticEvent, StyleProp, TextInput, TextInputFocusEventData, ViewStyle} from 'react-native';
diff --git a/src/components/TextInput/BaseTextInput/index.tsx b/src/components/TextInput/BaseTextInput/index.tsx
index e1862e0a6737..987890b9d16e 100644
--- a/src/components/TextInput/BaseTextInput/index.tsx
+++ b/src/components/TextInput/BaseTextInput/index.tsx
@@ -1,4 +1,4 @@
-import Str from 'expensify-common/lib/str';
+import {Str} from 'expensify-common';
import type {ForwardedRef} from 'react';
import React, {forwardRef, useCallback, useEffect, useMemo, useRef, useState} from 'react';
import type {GestureResponderEvent, LayoutChangeEvent, NativeSyntheticEvent, StyleProp, TextInputFocusEventData, ViewStyle} from 'react-native';
diff --git a/src/components/UnitPicker.tsx b/src/components/UnitPicker.tsx
index a9202a348e4d..02f056e04dad 100644
--- a/src/components/UnitPicker.tsx
+++ b/src/components/UnitPicker.tsx
@@ -1,4 +1,4 @@
-import Str from 'expensify-common/lib/str';
+import {Str} from 'expensify-common';
import React, {useMemo} from 'react';
import useLocalize from '@hooks/useLocalize';
import {getUnitTranslationKey} from '@libs/WorkspacesSettingsUtils';
diff --git a/src/components/UserDetailsTooltip/BaseUserDetailsTooltip/index.tsx b/src/components/UserDetailsTooltip/BaseUserDetailsTooltip/index.tsx
index 35c0b5e23f1a..f04b92644d89 100644
--- a/src/components/UserDetailsTooltip/BaseUserDetailsTooltip/index.tsx
+++ b/src/components/UserDetailsTooltip/BaseUserDetailsTooltip/index.tsx
@@ -1,4 +1,4 @@
-import Str from 'expensify-common/lib/str';
+import {Str} from 'expensify-common';
import React, {useCallback} from 'react';
import {View} from 'react-native';
import {useOnyx} from 'react-native-onyx';
diff --git a/src/components/createOnyxContext.tsx b/src/components/createOnyxContext.tsx
index 7cf802c57951..edd7100e57de 100644
--- a/src/components/createOnyxContext.tsx
+++ b/src/components/createOnyxContext.tsx
@@ -1,4 +1,4 @@
-import Str from 'expensify-common/lib/str';
+import {Str} from 'expensify-common';
import type {ComponentType, ForwardedRef, ForwardRefExoticComponent, PropsWithoutRef, ReactNode, RefAttributes} from 'react';
import React, {createContext, forwardRef, useContext} from 'react';
import type {OnyxValue} from 'react-native-onyx';
diff --git a/src/hooks/useCopySelectionHelper.ts b/src/hooks/useCopySelectionHelper.ts
index be7830dc6170..2018a2b178a8 100644
--- a/src/hooks/useCopySelectionHelper.ts
+++ b/src/hooks/useCopySelectionHelper.ts
@@ -1,4 +1,4 @@
-import ExpensiMark from 'expensify-common/lib/ExpensiMark';
+import {ExpensiMark} from 'expensify-common';
import {useEffect} from 'react';
import Clipboard from '@libs/Clipboard';
import KeyboardShortcut from '@libs/KeyboardShortcut';
diff --git a/src/hooks/useDeepCompareRef.ts b/src/hooks/useDeepCompareRef.ts
new file mode 100644
index 000000000000..7511c1516a1d
--- /dev/null
+++ b/src/hooks/useDeepCompareRef.ts
@@ -0,0 +1,24 @@
+import isEqual from 'lodash/isEqual';
+import {useRef} from 'react';
+
+/**
+ * This hook returns a reference to the provided value,
+ * but only updates that reference if a deep comparison indicates that the value has changed.
+ *
+ * This is useful when working with objects or arrays as dependencies to other hooks like `useEffect` or `useMemo`,
+ * where you want the hook to trigger not just on reference changes, but also when the contents of the object or array change.
+ *
+ * @example
+ * const myArray = // some array
+ * const deepComparedArray = useDeepCompareRef(myArray);
+ * useEffect(() => {
+ * // This will run not just when myArray is a new array, but also when its contents change.
+ * }, [deepComparedArray]);
+ */
+export default function useDeepCompareRef(value: T): T | undefined {
+ const ref = useRef();
+ if (!isEqual(value, ref.current)) {
+ ref.current = value;
+ }
+ return ref.current;
+}
diff --git a/src/hooks/useGeographicalStateFromRoute.ts b/src/hooks/useGeographicalStateFromRoute.ts
index 13936ee78f5b..434d4c534d61 100644
--- a/src/hooks/useGeographicalStateFromRoute.ts
+++ b/src/hooks/useGeographicalStateFromRoute.ts
@@ -1,6 +1,6 @@
import {useRoute} from '@react-navigation/native';
import type {ParamListBase, RouteProp} from '@react-navigation/native';
-import {CONST as COMMON_CONST} from 'expensify-common/lib/CONST';
+import {CONST as COMMON_CONST} from 'expensify-common';
type CustomParamList = ParamListBase & Record>;
type State = keyof typeof COMMON_CONST.STATES;
diff --git a/src/hooks/useHtmlPaste/index.ts b/src/hooks/useHtmlPaste/index.ts
index 925a3db518ae..a6d8993888cc 100644
--- a/src/hooks/useHtmlPaste/index.ts
+++ b/src/hooks/useHtmlPaste/index.ts
@@ -1,5 +1,5 @@
import {useNavigation} from '@react-navigation/native';
-import ExpensiMark from 'expensify-common/lib/ExpensiMark';
+import {ExpensiMark} from 'expensify-common';
import {useCallback, useEffect} from 'react';
import type UseHtmlPaste from './types';
diff --git a/src/languages/en.ts b/src/languages/en.ts
index 653697e271a7..1395be3f26c0 100755
--- a/src/languages/en.ts
+++ b/src/languages/en.ts
@@ -1,5 +1,4 @@
-import {CONST as COMMON_CONST} from 'expensify-common/lib/CONST';
-import Str from 'expensify-common/lib/str';
+import {CONST as COMMON_CONST, Str} from 'expensify-common';
import CONST from '@src/CONST';
import type {Country} from '@src/CONST';
import type {ConnectionName, PolicyConnectionSyncStage} from '@src/types/onyx/Policy';
@@ -165,6 +164,7 @@ export default {
continue: 'Continue',
firstName: 'First name',
lastName: 'Last name',
+ addCardTermsOfService: 'Expensify Terms of Service',
phone: 'Phone',
phoneNumber: 'Phone number',
phoneNumberPlaceholder: '(xxx) xxx-xxxx',
@@ -172,6 +172,7 @@ export default {
and: 'and',
details: 'Details',
privacy: 'Privacy',
+ privacyPolicy: 'Privacy Policy',
hidden: 'Hidden',
visible: 'Visible',
delete: 'Delete',
@@ -187,7 +188,6 @@ export default {
saveAndContinue: 'Save & continue',
settings: 'Settings',
termsOfService: 'Terms of Service',
- expensifyTermsOfService: 'Expensify Terms of Service',
members: 'Members',
invite: 'Invite',
here: 'here',
@@ -356,7 +356,7 @@ export default {
expensifyDoesntHaveAccessToCamera: "Expensify can't take photos without access to your camera. Tap Settings to update permissions.",
attachmentError: 'Attachment error',
errorWhileSelectingAttachment: 'An error occurred while selecting an attachment, please try again.',
- errorWhileSelectingCorruptedImage: 'An error occurred while selecting a corrupted attachment, please try another file.',
+ errorWhileSelectingCorruptedAttachment: 'An error occurred while selecting a corrupted attachment, please try another file.',
takePhoto: 'Take photo',
chooseFromGallery: 'Choose from gallery',
chooseDocument: 'Choose document',
@@ -762,6 +762,7 @@ export default {
reason: 'Reason',
holdReasonRequired: 'A reason is required when holding.',
expenseOnHold: 'This expense was put on hold. Review the comments for next steps.',
+ expensesOnHold: 'All expenses were put on hold. Review the comments for next steps.',
confirmApprove: 'Confirm approval amount',
confirmApprovalAmount: "Approve what's not on hold, or approve the entire report.",
confirmPay: 'Confirm payment amount',
@@ -1071,6 +1072,29 @@ export default {
password: 'Please enter your Expensify password.',
},
},
+ addPaymentCardPage: {
+ addAPaymentCard: 'Add payment card',
+ nameOnCard: 'Name on card',
+ paymentCardNumber: 'Card number',
+ expiration: 'Expiration date',
+ expirationDate: 'MMYY',
+ cvv: 'CVV',
+ billingAddress: 'Billing address',
+ growlMessageOnSave: 'Your payment card was successfully added',
+ expensifyPassword: 'Expensify password',
+ error: {
+ invalidName: 'Name can only include letters.',
+ addressZipCode: 'Please enter a valid zip code.',
+ paymentCardNumber: 'Please enter a valid card number.',
+ expirationDate: 'Please select a valid expiration date.',
+ securityCode: 'Please enter a valid security code.',
+ addressStreet: 'Please enter a valid billing address that is not a PO Box.',
+ addressState: 'Please select a state.',
+ addressCity: 'Please enter a city.',
+ genericFailureMessage: 'An error occurred while adding your card, please try again.',
+ password: 'Please enter your Expensify password.',
+ },
+ },
walletPage: {
paymentMethodsTitle: 'Payment methods',
setDefaultConfirmation: 'Make default payment method',
@@ -2259,6 +2283,8 @@ export default {
foreignDefault: 'Foreign currency default',
customTaxName: 'Custom tax name',
value: 'Value',
+ taxReclaimableOn: 'Tax reclaimable on',
+ taxRate: 'Tax rate',
error: {
taxRateAlreadyExists: 'This tax name is already in use.',
valuePercentageRange: 'Please enter a valid percentage between 0 and 100.',
@@ -2266,6 +2292,7 @@ export default {
deleteFailureMessage: 'An error occurred while deleting the tax rate. Please try again or ask Concierge for help.',
updateFailureMessage: 'An error occurred while updating the tax rate. Please try again or ask Concierge for help.',
createFailureMessage: 'An error occurred while creating the tax rate. Please try again or ask Concierge for help.',
+ updateTaxClaimableFailureMessage: 'The reclaimable portion must be less than the distance rate amount.',
},
deleteTaxConfirmation: 'Are you sure you want to delete this tax?',
deleteMultipleTaxConfirmation: ({taxAmount}) => `Are you sure you want to delete ${taxAmount} taxes?`,
@@ -2555,12 +2582,15 @@ export default {
centrallyManage: 'Centrally manage rates, choose to track in miles or kilometers, and set a default category.',
rate: 'Rate',
addRate: 'Add rate',
+ trackTax: 'Track tax',
deleteRates: ({count}: DistanceRateOperationsParams) => `Delete ${Str.pluralize('rate', 'rates', count)}`,
enableRates: ({count}: DistanceRateOperationsParams) => `Enable ${Str.pluralize('rate', 'rates', count)}`,
disableRates: ({count}: DistanceRateOperationsParams) => `Disable ${Str.pluralize('rate', 'rates', count)}`,
enableRate: 'Enable rate',
status: 'Status',
unit: 'Unit',
+ taxFeatureNotEnabledMessage: 'Taxes must be enabled on the workspace to use this feature. Head over to ',
+ changePromptMessage: ' to make that change.',
defaultCategory: 'Default category',
deleteDistanceRate: 'Delete distance rate',
areYouSureDelete: ({count}: DistanceRateOperationsParams) => `Are you sure you want to delete ${Str.pluralize('this rate', 'these rates', count)}?`,
@@ -3228,5 +3258,11 @@ export default {
size: 'Please enter a valid subscription size.',
},
},
+ paymentCard: {
+ addPaymentCard: 'Add payment card',
+ enterPaymentCardDetails: 'Enter your payment card details.',
+ security: 'Expensify is PCI-DSS compliant, uses bank-level encryption, and utilizes redundant infrastructure to protect your data.',
+ learnMoreAboutSecurity: 'Learn more about our security.',
+ },
},
} satisfies TranslationBase;
diff --git a/src/languages/es.ts b/src/languages/es.ts
index 5d184677655e..7566ca7b7642 100644
--- a/src/languages/es.ts
+++ b/src/languages/es.ts
@@ -1,4 +1,4 @@
-import Str from 'expensify-common/lib/str';
+import {Str} from 'expensify-common';
import CONST from '@src/CONST';
import type {ConnectionName, PolicyConnectionSyncStage} from '@src/types/onyx/Policy';
import type {
@@ -148,6 +148,8 @@ export default {
preferences: 'Preferencias',
view: 'Ver',
not: 'No',
+ privacyPolicy: 'la Política de Privacidad de Expensify',
+ addCardTermsOfService: 'Términos de Servicio',
signIn: 'Conectarse',
signInWithGoogle: 'Iniciar sesión con Google',
signInWithApple: 'Iniciar sesión con Apple',
@@ -177,7 +179,6 @@ export default {
saveAndContinue: 'Guardar y continuar',
settings: 'Configuración',
termsOfService: 'Términos de Servicio',
- expensifyTermsOfService: 'Términos de Servicio de Expensify',
members: 'Miembros',
invite: 'Invitar',
here: 'aquí',
@@ -350,7 +351,7 @@ export default {
expensifyDoesntHaveAccessToCamera: 'Expensify no puede tomar fotos sin acceso a la cámara. Haz click en Configuración para actualizar los permisos.',
attachmentError: 'Error al adjuntar archivo',
errorWhileSelectingAttachment: 'Ha ocurrido un error al seleccionar un archivo adjunto. Por favor, inténtalo de nuevo.',
- errorWhileSelectingCorruptedImage: 'Ha ocurrido un error al seleccionar un archivo adjunto corrupto. Por favor, inténtalo con otro archivo.',
+ errorWhileSelectingCorruptedAttachment: 'Ha ocurrido un error al seleccionar un archivo adjunto corrupto. Por favor, inténtalo con otro archivo.',
takePhoto: 'Hacer una foto',
chooseFromGallery: 'Elegir de la galería',
chooseDocument: 'Elegir documento',
@@ -756,6 +757,7 @@ export default {
reason: 'Razón',
holdReasonRequired: 'Se requiere una razón para bloquear.',
expenseOnHold: 'Este gasto está bloqueado. Revisa los comentarios para saber como proceder.',
+ expensesOnHold: 'Todos los gastos quedaron bloqueado. Revisa los comentarios para saber como proceder.',
confirmApprove: 'Confirmar importe a aprobar',
confirmApprovalAmount: 'Aprueba lo que no está bloqueado, o aprueba todo el informe.',
confirmPay: 'Confirmar importe de pago',
@@ -1067,6 +1069,29 @@ export default {
password: 'Por favor, introduce tu contraseña de Expensify.',
},
},
+ addPaymentCardPage: {
+ addAPaymentCard: 'Añade tarjeta de pago',
+ nameOnCard: 'Nombre en la tarjeta',
+ paymentCardNumber: 'Número de la tarjeta',
+ expiration: 'Fecha de vencimiento',
+ expirationDate: 'MMAA',
+ cvv: 'CVV',
+ billingAddress: 'Dirección de envio',
+ growlMessageOnSave: 'Tu tarjeta de pago se añadió correctamente',
+ expensifyPassword: 'Contraseña de Expensify',
+ error: {
+ invalidName: 'El nombre sólo puede incluir letras.',
+ addressZipCode: 'Por favor, introduce un código postal válido.',
+ paymentCardNumber: 'Por favor, introduce un número de tarjeta de pago válido.',
+ expirationDate: 'Por favor, selecciona una fecha de vencimiento válida.',
+ securityCode: 'Por favor, introduce un código de seguridad válido.',
+ addressStreet: 'Por favor, introduce una dirección de facturación válida que no sea un apartado postal.',
+ addressState: 'Por favor, selecciona un estado.',
+ addressCity: 'Por favor, introduce una ciudad.',
+ genericFailureMessage: 'Se produjo un error al añadir tu tarjeta. Vuelva a intentarlo.',
+ password: 'Por favor, introduce tu contraseña de Expensify.',
+ },
+ },
walletPage: {
paymentMethodsTitle: 'Métodos de pago',
setDefaultConfirmation: 'Marcar como método de pago predeterminado',
@@ -2297,6 +2322,8 @@ export default {
foreignDefault: 'Moneda extranjera por defecto',
customTaxName: 'Nombre del impuesto',
value: 'Valor',
+ taxRate: 'Tasa de impuesto',
+ taxReclaimableOn: 'Impuesto recuperable en',
error: {
taxRateAlreadyExists: 'Ya existe un impuesto con este nombre.',
customNameRequired: 'El nombre del impuesto es obligatorio.',
@@ -2304,6 +2331,7 @@ export default {
deleteFailureMessage: 'Se ha producido un error al intentar eliminar la tasa de impuesto. Por favor, inténtalo más tarde.',
updateFailureMessage: 'Se ha producido un error al intentar modificar la tasa de impuesto. Por favor, inténtalo más tarde.',
createFailureMessage: 'Se ha producido un error al intentar crear la tasa de impuesto. Por favor, inténtalo más tarde.',
+ updateTaxClaimableFailureMessage: 'La porción recuperable debe ser menor al monto del importe por distancia.',
},
deleteTaxConfirmation: '¿Estás seguro de que quieres eliminar este impuesto?',
deleteMultipleTaxConfirmation: ({taxAmount}) => `¿Estás seguro de que quieres eliminar ${taxAmount} impuestos?`,
@@ -2594,12 +2622,15 @@ export default {
centrallyManage: 'Gestiona centralizadamente las tasas, elige si contabilizar en millas o kilómetros, y define una categoría por defecto',
rate: 'Tasa',
addRate: 'Agregar tasa',
+ trackTax: 'Impuesto de seguimiento',
deleteRates: ({count}: DistanceRateOperationsParams) => `Eliminar ${Str.pluralize('tasa', 'tasas', count)}`,
enableRates: ({count}: DistanceRateOperationsParams) => `Activar ${Str.pluralize('tasa', 'tasas', count)}`,
disableRates: ({count}: DistanceRateOperationsParams) => `Desactivar ${Str.pluralize('tasa', 'tasas', count)}`,
enableRate: 'Activar tasa',
status: 'Estado',
unit: 'Unidad',
+ taxFeatureNotEnabledMessage: 'Los impuestos deben estar activados en el área de trabajo para poder utilizar esta función. Dirígete a ',
+ changePromptMessage: ' para hacer ese cambio.',
defaultCategory: 'Categoría predeterminada',
deleteDistanceRate: 'Eliminar tasa de distancia',
areYouSureDelete: ({count}: DistanceRateOperationsParams) => `¿Estás seguro de que quieres eliminar ${Str.pluralize('esta tasa', 'estas tasas', count)}?`,
@@ -3734,5 +3765,11 @@ export default {
size: 'Por favor ingrese un tamaño de suscripción valido.',
},
},
+ paymentCard: {
+ addPaymentCard: 'Añade tarjeta de pago',
+ enterPaymentCardDetails: 'Introduce los datos de tu tarjeta de pago.',
+ security: 'Expensify es PCI-DSS obediente, utiliza cifrado a nivel bancario, y emplea infraestructura redundante para proteger tus datos.',
+ learnMoreAboutSecurity: 'Conozca más sobre nuestra seguridad.',
+ },
},
} satisfies EnglishTranslation;
diff --git a/src/libs/API/parameters/AddSubscriptionPaymentCardParams.ts b/src/libs/API/parameters/AddSubscriptionPaymentCardParams.ts
new file mode 100644
index 000000000000..ef8fbc382737
--- /dev/null
+++ b/src/libs/API/parameters/AddSubscriptionPaymentCardParams.ts
@@ -0,0 +1,11 @@
+type AddSubscriptionPaymentCardParams = {
+ cardNumber: string;
+ cardYear: string;
+ cardMonth: string;
+ cardCVV: string;
+ addressName: string;
+ addressZip: string;
+ currency: string;
+};
+
+export default AddSubscriptionPaymentCardParams;
diff --git a/src/libs/API/parameters/SendInvoiceParams.ts b/src/libs/API/parameters/SendInvoiceParams.ts
index 55a82e310399..f8ba0647fb0a 100644
--- a/src/libs/API/parameters/SendInvoiceParams.ts
+++ b/src/libs/API/parameters/SendInvoiceParams.ts
@@ -1,20 +1,25 @@
-type SendInvoiceParams = {
- senderWorkspaceID: string;
- accountID: number;
- receiverEmail?: string;
- receiverInvoiceRoomID?: string;
- amount: number;
- currency: string;
- comment: string;
- merchant: string;
- date: string;
- category?: string;
- invoiceRoomReportID?: string;
- createdChatReportActionID: string;
- invoiceReportID: string;
- reportPreviewReportActionID: string;
- transactionID: string;
- transactionThreadReportID: string;
-};
+import type {RequireAtLeastOne} from 'type-fest';
+
+type SendInvoiceParams = RequireAtLeastOne<
+ {
+ senderWorkspaceID: string;
+ accountID: number;
+ receiverEmail?: string;
+ receiverInvoiceRoomID?: string;
+ amount: number;
+ currency: string;
+ comment: string;
+ merchant: string;
+ date: string;
+ category?: string;
+ invoiceRoomReportID?: string;
+ createdChatReportActionID: string;
+ invoiceReportID: string;
+ reportPreviewReportActionID: string;
+ transactionID: string;
+ transactionThreadReportID: string;
+ },
+ 'receiverEmail' | 'receiverInvoiceRoomID'
+>;
export default SendInvoiceParams;
diff --git a/src/libs/API/parameters/index.ts b/src/libs/API/parameters/index.ts
index 6fa58f44bd89..de8d6f55418b 100644
--- a/src/libs/API/parameters/index.ts
+++ b/src/libs/API/parameters/index.ts
@@ -205,6 +205,7 @@ export type {default as DeletePolicyTaxesParams} from './DeletePolicyTaxesParams
export type {default as UpdatePolicyTaxValueParams} from './UpdatePolicyTaxValueParams';
export type {default as RenamePolicyTagsParams} from './RenamePolicyTagsParams';
export type {default as DeletePolicyTagsParams} from './DeletePolicyTagsParams';
+export type {default as AddSubscriptionPaymentCardParams} from './AddSubscriptionPaymentCardParams';
export type {default as SetPolicyCustomTaxNameParams} from './SetPolicyCustomTaxNameParams';
export type {default as SetPolicyForeignCurrencyDefaultParams} from './SetPolicyForeignCurrencyDefaultParams';
export type {default as SetPolicyCurrencyDefaultParams} from './SetPolicyCurrencyDefaultParams';
diff --git a/src/libs/API/types.ts b/src/libs/API/types.ts
index 6ca80d525682..3f1acaaed466 100644
--- a/src/libs/API/types.ts
+++ b/src/libs/API/types.ts
@@ -206,7 +206,10 @@ const WRITE_COMMANDS = {
ADD_BILLING_CARD_AND_REQUEST_WORKSPACE_OWNER_CHANGE: 'AddBillingCardAndRequestPolicyOwnerChange',
SET_POLICY_DISTANCE_RATES_UNIT: 'SetPolicyDistanceRatesUnit',
SET_POLICY_DISTANCE_RATES_DEFAULT_CATEGORY: 'SetPolicyDistanceRatesDefaultCategory',
+ ENABLE_DISTANCE_REQUEST_TAX: 'EnableDistanceRequestTax',
UPDATE_POLICY_DISTANCE_RATE_VALUE: 'UpdatePolicyDistanceRateValue',
+ UPDATE_POLICY_DISTANCE_TAX_RATE_VALUE: 'UpdateDistanceTaxRate',
+ UPDATE_DISTANCE_TAX_CLAIMABLE_VALUE: 'UpdateDistanceTaxClaimableValue',
SET_POLICY_DISTANCE_RATES_ENABLED: 'SetPolicyDistanceRatesEnabled',
DELETE_POLICY_DISTANCE_RATES: 'DeletePolicyDistanceRates',
DISMISS_TRACK_EXPENSE_ACTIONABLE_WHISPER: 'DismissActionableWhisper',
@@ -419,12 +422,15 @@ type WriteCommandParameters = {
[WRITE_COMMANDS.RENAME_POLICY_TAX]: Parameters.RenamePolicyTaxParams;
[WRITE_COMMANDS.SET_POLICY_DISTANCE_RATES_UNIT]: Parameters.SetPolicyDistanceRatesUnitParams;
[WRITE_COMMANDS.SET_POLICY_DISTANCE_RATES_DEFAULT_CATEGORY]: Parameters.SetPolicyDistanceRatesDefaultCategoryParams;
+ [WRITE_COMMANDS.ENABLE_DISTANCE_REQUEST_TAX]: Parameters.SetPolicyDistanceRatesDefaultCategoryParams;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
[WRITE_COMMANDS.UPDATE_POLICY_CONNECTION_CONFIG]: Parameters.UpdatePolicyConnectionConfigParams;
[WRITE_COMMANDS.UPDATE_MANY_POLICY_CONNECTION_CONFIGS]: Parameters.UpdateManyPolicyConnectionConfigurationsParams;
[WRITE_COMMANDS.REMOVE_POLICY_CONNECTION]: Parameters.RemovePolicyConnectionParams;
[WRITE_COMMANDS.UPDATE_POLICY_DISTANCE_RATE_VALUE]: Parameters.UpdatePolicyDistanceRateValueParams;
+ [WRITE_COMMANDS.UPDATE_POLICY_DISTANCE_TAX_RATE_VALUE]: Parameters.UpdatePolicyDistanceRateValueParams;
+ [WRITE_COMMANDS.UPDATE_DISTANCE_TAX_CLAIMABLE_VALUE]: Parameters.UpdatePolicyDistanceRateValueParams;
[WRITE_COMMANDS.SET_POLICY_DISTANCE_RATES_ENABLED]: Parameters.SetPolicyDistanceRatesEnabledParams;
[WRITE_COMMANDS.DELETE_POLICY_DISTANCE_RATES]: Parameters.DeletePolicyDistanceRatesParams;
[WRITE_COMMANDS.DISMISS_TRACK_EXPENSE_ACTIONABLE_WHISPER]: Parameters.DismissTrackExpenseActionableWhisperParams;
diff --git a/src/libs/ActiveClientManager/index.ts b/src/libs/ActiveClientManager/index.ts
index 69bd3a848e0b..e93cdb07d084 100644
--- a/src/libs/ActiveClientManager/index.ts
+++ b/src/libs/ActiveClientManager/index.ts
@@ -3,7 +3,7 @@
* only one tab is processing those saved requests or we would be duplicating data (or creating errors).
* This file ensures exactly that by tracking all the clientIDs connected, storing the most recent one last and it considers that last clientID the "leader".
*/
-import Str from 'expensify-common/lib/str';
+import {Str} from 'expensify-common';
import Onyx from 'react-native-onyx';
import * as ActiveClients from '@userActions/ActiveClients';
import ONYXKEYS from '@src/ONYXKEYS';
diff --git a/src/libs/BankAccountUtils.ts b/src/libs/BankAccountUtils.ts
index a7fbc5f3bd4e..d3c5bb4998a8 100644
--- a/src/libs/BankAccountUtils.ts
+++ b/src/libs/BankAccountUtils.ts
@@ -1,4 +1,4 @@
-import Str from 'expensify-common/lib/str';
+import {Str} from 'expensify-common';
import type {OnyxEntry} from 'react-native-onyx';
import type * as OnyxTypes from '@src/types/onyx';
diff --git a/src/libs/EmojiUtils.ts b/src/libs/EmojiUtils.ts
index 0be7e76a0aa9..db7d16da21f3 100644
--- a/src/libs/EmojiUtils.ts
+++ b/src/libs/EmojiUtils.ts
@@ -1,5 +1,5 @@
import {getUnixTime} from 'date-fns';
-import Str from 'expensify-common/lib/str';
+import {Str} from 'expensify-common';
import memoize from 'lodash/memoize';
import Onyx from 'react-native-onyx';
import type {OnyxEntry} from 'react-native-onyx';
diff --git a/src/libs/GetStyledTextArray.ts b/src/libs/GetStyledTextArray.ts
index ffae31dc861b..9eb8971c81bd 100644
--- a/src/libs/GetStyledTextArray.ts
+++ b/src/libs/GetStyledTextArray.ts
@@ -1,4 +1,4 @@
-import Str from 'expensify-common/lib/str';
+import {Str} from 'expensify-common';
import StringUtils from './StringUtils';
type StyledText = {
diff --git a/src/libs/KeyboardShortcut/index.ts b/src/libs/KeyboardShortcut/index.ts
index b349c43b5715..4f89d44da959 100644
--- a/src/libs/KeyboardShortcut/index.ts
+++ b/src/libs/KeyboardShortcut/index.ts
@@ -1,4 +1,4 @@
-import Str from 'expensify-common/lib/str';
+import {Str} from 'expensify-common';
import * as KeyCommand from 'react-native-key-command';
import getOperatingSystem from '@libs/getOperatingSystem';
import localeCompare from '@libs/LocaleCompare';
diff --git a/src/libs/LocalePhoneNumber.ts b/src/libs/LocalePhoneNumber.ts
index 460d5fc0fe9f..7249912e6f58 100644
--- a/src/libs/LocalePhoneNumber.ts
+++ b/src/libs/LocalePhoneNumber.ts
@@ -1,4 +1,4 @@
-import Str from 'expensify-common/lib/str';
+import {Str} from 'expensify-common';
import Onyx from 'react-native-onyx';
import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
diff --git a/src/libs/Log.ts b/src/libs/Log.ts
index 101996870e1d..c0bbbfd3f60a 100644
--- a/src/libs/Log.ts
+++ b/src/libs/Log.ts
@@ -2,7 +2,7 @@
// action would likely cause confusion about which one to use. But most other API methods should happen inside an action file.
/* eslint-disable rulesdir/no-api-in-views */
-import Logger from 'expensify-common/lib/Logger';
+import {ExpensiMark, Logger} from 'expensify-common';
import Onyx from 'react-native-onyx';
import type {Merge} from 'type-fest';
import CONST from '@src/CONST';
@@ -80,5 +80,6 @@ const Log = new Logger({
isDebug: true,
});
timeout = setTimeout(() => Log.info('Flushing logs older than 10 minutes', true, {}, true), 10 * 60 * 1000);
+ExpensiMark.setLogger(Log);
export default Log;
diff --git a/src/libs/LoginUtils.ts b/src/libs/LoginUtils.ts
index 2d49cb992717..ded60ab3e800 100644
--- a/src/libs/LoginUtils.ts
+++ b/src/libs/LoginUtils.ts
@@ -1,5 +1,4 @@
-import {PUBLIC_DOMAINS} from 'expensify-common/lib/CONST';
-import Str from 'expensify-common/lib/str';
+import {PUBLIC_DOMAINS, Str} from 'expensify-common';
import Onyx from 'react-native-onyx';
import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
diff --git a/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx b/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx
index 67890a132d2d..9a391e386f15 100644
--- a/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx
+++ b/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx
@@ -247,6 +247,9 @@ const SettingsModalStackNavigator = createModalStackNavigator require('../../../../pages/workspace/distanceRates/PolicyDistanceRatesSettingsPage').default as React.ComponentType,
[SCREENS.WORKSPACE.DISTANCE_RATE_DETAILS]: () => require('../../../../pages/workspace/distanceRates/PolicyDistanceRateDetailsPage').default as React.ComponentType,
[SCREENS.WORKSPACE.DISTANCE_RATE_EDIT]: () => require('../../../../pages/workspace/distanceRates/PolicyDistanceRateEditPage').default as React.ComponentType,
+ [SCREENS.WORKSPACE.DISTANCE_RATE_TAX_RECLAIMABLE_ON_EDIT]: () =>
+ require('../../../../pages/workspace/distanceRates/PolicyDistanceRateTaxReclaimableEditPage').default as React.ComponentType,
+ [SCREENS.WORKSPACE.DISTANCE_RATE_TAX_RATE_EDIT]: () => require('../../../../pages/workspace/distanceRates/PolicyDistanceRateTaxRateEditPage').default as React.ComponentType,
[SCREENS.WORKSPACE.TAGS_SETTINGS]: () => require('../../../../pages/workspace/tags/WorkspaceTagsSettingsPage').default as React.ComponentType,
[SCREENS.WORKSPACE.TAG_SETTINGS]: () => require('../../../../pages/workspace/tags/TagSettingsPage').default as React.ComponentType,
[SCREENS.WORKSPACE.TAG_LIST_VIEW]: () => require('../../../../pages/workspace/tags/WorkspaceViewTagsPage').default as React.ComponentType,
@@ -331,6 +334,7 @@ const SettingsModalStackNavigator = createModalStackNavigator require('../../../../pages/workspace/taxes/ValuePage').default as React.ComponentType,
[SCREENS.WORKSPACE.TAX_CREATE]: () => require('../../../../pages/workspace/taxes/WorkspaceCreateTaxPage').default as React.ComponentType,
[SCREENS.SETTINGS.SAVE_THE_WORLD]: () => require('../../../../pages/TeachersUnite/SaveTheWorldPage').default as React.ComponentType,
+ [SCREENS.SETTINGS.SUBSCRIPTION.ADD_PAYMENT_CARD]: () => require('../../../../pages/settings/Subscription/PaymentCard/AddPaymentCard').default as React.ComponentType,
});
const EnablePaymentsStackNavigator = createModalStackNavigator({
diff --git a/src/libs/Navigation/linkTo/index.ts b/src/libs/Navigation/linkTo/index.ts
index b9839483a056..424856c1fafc 100644
--- a/src/libs/Navigation/linkTo/index.ts
+++ b/src/libs/Navigation/linkTo/index.ts
@@ -80,13 +80,8 @@ export default function linkTo(navigation: NavigationContainerRef | undefined, (value) => value === undefined),
);
- // If the type is UP, we deeplinked into one of the RHP flows and we want to replace the current screen with the previous one in the flow
- // and at the same time we want the back button to go to the page we were before the deeplink
- if (type === CONST.NAVIGATION.TYPE.UP) {
- action.type = CONST.NAVIGATION.ACTION_TYPE.REPLACE;
-
- // If this action is navigating to the report screen and the top most navigator is different from the one we want to navigate - PUSH the new screen to the top of the stack
- } else if (action.payload.name === NAVIGATORS.CENTRAL_PANE_NAVIGATOR && (isTargetScreenDifferentThanCurrent || areParamsDifferent)) {
+ // If this action is navigating to the report screen and the top most navigator is different from the one we want to navigate - PUSH the new screen to the top of the stack by default
+ if (action.payload.name === NAVIGATORS.CENTRAL_PANE_NAVIGATOR && (isTargetScreenDifferentThanCurrent || areParamsDifferent)) {
// We need to push a tab if the tab doesn't match the central pane route that we are going to push.
const topmostBottomTabRoute = getTopmostBottomTabRoute(rootState);
const policyIDsFromState = extractPolicyIDsFromState(stateFromPath);
@@ -100,13 +95,22 @@ export default function linkTo(navigation: NavigationContainerRef> =
[SCREENS.SETTINGS.SAVE_THE_WORLD]: [SCREENS.I_KNOW_A_TEACHER, SCREENS.INTRO_SCHOOL_PRINCIPAL, SCREENS.I_AM_A_TEACHER],
[SCREENS.SETTINGS.TROUBLESHOOT]: [SCREENS.SETTINGS.CONSOLE],
[SCREENS.SEARCH.CENTRAL_PANE]: [SCREENS.SEARCH.REPORT_RHP],
- [SCREENS.SETTINGS.SUBSCRIPTION.ROOT]: [SCREENS.SETTINGS.SUBSCRIPTION.SIZE],
+ [SCREENS.SETTINGS.SUBSCRIPTION.ROOT]: [SCREENS.SETTINGS.SUBSCRIPTION.ADD_PAYMENT_CARD, SCREENS.SETTINGS.SUBSCRIPTION.SIZE],
};
export default CENTRAL_PANE_TO_RHP_MAPPING;
diff --git a/src/libs/Navigation/linkingConfig/FULL_SCREEN_TO_RHP_MAPPING.ts b/src/libs/Navigation/linkingConfig/FULL_SCREEN_TO_RHP_MAPPING.ts
index 2aaceb96f52a..f91d290639ff 100755
--- a/src/libs/Navigation/linkingConfig/FULL_SCREEN_TO_RHP_MAPPING.ts
+++ b/src/libs/Navigation/linkingConfig/FULL_SCREEN_TO_RHP_MAPPING.ts
@@ -79,6 +79,8 @@ const FULL_SCREEN_TO_RHP_MAPPING: Partial> = {
SCREENS.WORKSPACE.CREATE_DISTANCE_RATE,
SCREENS.WORKSPACE.DISTANCE_RATES_SETTINGS,
SCREENS.WORKSPACE.DISTANCE_RATE_EDIT,
+ SCREENS.WORKSPACE.DISTANCE_RATE_TAX_RECLAIMABLE_ON_EDIT,
+ SCREENS.WORKSPACE.DISTANCE_RATE_TAX_RATE_EDIT,
SCREENS.WORKSPACE.DISTANCE_RATE_DETAILS,
],
};
diff --git a/src/libs/Navigation/linkingConfig/config.ts b/src/libs/Navigation/linkingConfig/config.ts
index b19fbc4c38e0..b3f27d422b4b 100644
--- a/src/libs/Navigation/linkingConfig/config.ts
+++ b/src/libs/Navigation/linkingConfig/config.ts
@@ -126,6 +126,10 @@ const config: LinkingOptions['config'] = {
path: ROUTES.SETTINGS_LANGUAGE,
exact: true,
},
+ [SCREENS.SETTINGS.SUBSCRIPTION.ADD_PAYMENT_CARD]: {
+ path: ROUTES.SETTINGS_SUBSCRIPTION_ADD_PAYMENT_CARD,
+ exact: true,
+ },
[SCREENS.SETTINGS.PREFERENCES.THEME]: {
path: ROUTES.SETTINGS_THEME,
exact: true,
@@ -416,6 +420,12 @@ const config: LinkingOptions['config'] = {
[SCREENS.WORKSPACE.DISTANCE_RATE_EDIT]: {
path: ROUTES.WORKSPACE_DISTANCE_RATE_EDIT.route,
},
+ [SCREENS.WORKSPACE.DISTANCE_RATE_TAX_RECLAIMABLE_ON_EDIT]: {
+ path: ROUTES.WORKSPACE_DISTANCE_RATE_TAX_RECLAIMABLE_ON_EDIT.route,
+ },
+ [SCREENS.WORKSPACE.DISTANCE_RATE_TAX_RATE_EDIT]: {
+ path: ROUTES.WORKSPACE_DISTANCE_RATE_TAX_RATE_EDIT.route,
+ },
[SCREENS.WORKSPACE.TAGS_SETTINGS]: {
path: ROUTES.WORKSPACE_TAGS_SETTINGS.route,
},
diff --git a/src/libs/Navigation/switchPolicyID.ts b/src/libs/Navigation/switchPolicyID.ts
index c526773c3fce..78d23cb9d53c 100644
--- a/src/libs/Navigation/switchPolicyID.ts
+++ b/src/libs/Navigation/switchPolicyID.ts
@@ -75,7 +75,7 @@ export default function switchPolicyID(navigation: NavigationContainerRef, transactionViolations: OnyxCollection) {
+ if (!Permissions.canUseViolations(betas)) {
+ return false;
+ }
+ const {parentReportID, parentReportActionID} = report ?? {};
+ const canGetParentReport = parentReportID && parentReportActionID && allReportActions;
+ if (!canGetParentReport) {
+ return false;
+ }
+ const parentReportActions = allReportActions ? allReportActions[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${parentReportID}`] ?? {} : {};
+ const parentReportAction = parentReportActions[parentReportActionID] ?? null;
+ if (!parentReportAction) {
+ return false;
+ }
+ return ReportUtils.shouldDisplayTransactionThreadViolations(report, transactionViolations, parentReportAction);
+}
+
/**
* filter options based on specific conditions
*/
@@ -1791,13 +1811,7 @@ function getOptions(
// Filter out all the reports that shouldn't be displayed
const filteredReportOptions = options.reports.filter((option) => {
const report = option.item;
-
- const {parentReportID, parentReportActionID} = report ?? {};
- const canGetParentReport = parentReportID && parentReportActionID && allReportActions;
- const parentReportActions = allReportActions ? allReportActions[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${parentReportID}`] ?? {} : {};
- const parentReportAction = canGetParentReport ? parentReportActions[parentReportActionID] ?? null : null;
- const doesReportHaveViolations =
- (betas?.includes(CONST.BETAS.VIOLATIONS) && ReportUtils.doesTransactionThreadHaveViolations(report, transactionViolations, parentReportAction)) ?? false;
+ const doesReportHaveViolations = shouldShowViolations(report, betas, transactionViolations);
return ReportUtils.shouldReportBeInOptionList({
report,
@@ -2481,6 +2495,7 @@ export {
getTaxRatesSection,
getFirstKeyForList,
getUserToInviteOption,
+ shouldShowViolations,
};
export type {MemberForList, CategorySection, CategoryTreeSection, Options, OptionList, SearchOption, PayeePersonalDetails, Category, Tax, TaxRatesOption, Option, OptionTree};
diff --git a/src/libs/PersonalDetailsUtils.ts b/src/libs/PersonalDetailsUtils.ts
index d58ac4d5218c..8cea00a28d94 100644
--- a/src/libs/PersonalDetailsUtils.ts
+++ b/src/libs/PersonalDetailsUtils.ts
@@ -1,4 +1,4 @@
-import Str from 'expensify-common/lib/str';
+import {Str} from 'expensify-common';
import type {OnyxEntry, OnyxUpdate} from 'react-native-onyx';
import Onyx from 'react-native-onyx';
import type {CurrentUserPersonalDetails} from '@components/withCurrentUserPersonalDetails';
diff --git a/src/libs/PhoneNumber.ts b/src/libs/PhoneNumber.ts
index 787b3634030a..84ef35a18489 100644
--- a/src/libs/PhoneNumber.ts
+++ b/src/libs/PhoneNumber.ts
@@ -1,7 +1,7 @@
// eslint-disable-next-line no-restricted-imports
import {parsePhoneNumber as originalParsePhoneNumber} from 'awesome-phonenumber';
import type {ParsedPhoneNumber, ParsedPhoneNumberInvalid, PhoneNumberParseOptions} from 'awesome-phonenumber';
-import Str from 'expensify-common/lib/str';
+import {Str} from 'expensify-common';
import CONST from '@src/CONST';
/**
diff --git a/src/libs/PolicyDistanceRatesUtils.ts b/src/libs/PolicyDistanceRatesUtils.ts
index db739cc3b8c7..084356df7449 100644
--- a/src/libs/PolicyDistanceRatesUtils.ts
+++ b/src/libs/PolicyDistanceRatesUtils.ts
@@ -9,6 +9,8 @@ import * as NumberUtils from './NumberUtils';
type RateValueForm = typeof ONYXKEYS.FORMS.WORKSPACE_RATE_AND_UNIT_FORM | typeof ONYXKEYS.FORMS.POLICY_CREATE_DISTANCE_RATE_FORM | typeof ONYXKEYS.FORMS.POLICY_DISTANCE_RATE_EDIT_FORM;
+type TaxReclaimableForm = typeof ONYXKEYS.FORMS.POLICY_DISTANCE_RATE_TAX_RECLAIMABLE_ON_EDIT_FORM;
+
function validateRateValue(values: FormOnyxValues, currency: string, toLocaleDigit: (arg: string) => string): FormInputErrors {
const errors: FormInputErrors = {};
const parsedRate = MoneyRequestUtils.replaceAllDigits(values.rate, toLocaleDigit);
@@ -24,6 +26,15 @@ function validateRateValue(values: FormOnyxValues, currency: stri
return errors;
}
+function validateTaxClaimableValue(values: FormOnyxValues, rate: Rate): FormInputErrors {
+ const errors: FormInputErrors = {};
+
+ if (rate.rate && Number(values.taxClaimableValue) > rate.rate / 100) {
+ errors.taxClaimableValue = 'workspace.taxes.error.updateTaxClaimableFailureMessage';
+ }
+ return errors;
+}
+
/**
* Get the optimistic rate name in a way that matches BE logic
* @param rates
@@ -33,4 +44,4 @@ function getOptimisticRateName(rates: Record): string {
return existingRatesWithSameName.length ? `${CONST.CUSTOM_UNITS.DEFAULT_RATE} ${existingRatesWithSameName.length}` : CONST.CUSTOM_UNITS.DEFAULT_RATE;
}
-export {validateRateValue, getOptimisticRateName};
+export {validateRateValue, getOptimisticRateName, validateTaxClaimableValue};
diff --git a/src/libs/PolicyUtils.ts b/src/libs/PolicyUtils.ts
index 553e775ba99f..321cc5f1c7d0 100644
--- a/src/libs/PolicyUtils.ts
+++ b/src/libs/PolicyUtils.ts
@@ -1,4 +1,4 @@
-import Str from 'expensify-common/lib/str';
+import {Str} from 'expensify-common';
import type {OnyxCollection, OnyxEntry} from 'react-native-onyx';
import Onyx from 'react-native-onyx';
import type {ValueOf} from 'type-fest';
diff --git a/src/libs/ReceiptUtils.ts b/src/libs/ReceiptUtils.ts
index 849bc50e77b0..b3892d20162f 100644
--- a/src/libs/ReceiptUtils.ts
+++ b/src/libs/ReceiptUtils.ts
@@ -1,4 +1,4 @@
-import Str from 'expensify-common/lib/str';
+import {Str} from 'expensify-common';
import _ from 'lodash';
import type {OnyxEntry} from 'react-native-onyx';
import CONST from '@src/CONST';
diff --git a/src/libs/ReportActionsUtils.ts b/src/libs/ReportActionsUtils.ts
index 1cfec72d88e0..58453eba585f 100644
--- a/src/libs/ReportActionsUtils.ts
+++ b/src/libs/ReportActionsUtils.ts
@@ -1,4 +1,4 @@
-import fastMerge from 'expensify-common/lib/fastMerge';
+import {fastMerge} from 'expensify-common';
import _ from 'lodash';
import lodashFindLast from 'lodash/findLast';
import type {OnyxCollection, OnyxEntry, OnyxUpdate} from 'react-native-onyx';
diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts
index d82fb224784b..69408b576506 100644
--- a/src/libs/ReportUtils.ts
+++ b/src/libs/ReportUtils.ts
@@ -1,6 +1,5 @@
import {format} from 'date-fns';
-import ExpensiMark from 'expensify-common/lib/ExpensiMark';
-import Str from 'expensify-common/lib/str';
+import {ExpensiMark, Str} from 'expensify-common';
import {isEmpty} from 'lodash';
import lodashEscape from 'lodash/escape';
import lodashFindLastIndex from 'lodash/findLastIndex';
@@ -3867,7 +3866,7 @@ function getIOUSubmittedMessage(report: OnyxEntry) {
];
}
- const submittedToPersonalDetail = getPersonalDetailsForAccountID(policy?.submitsTo ?? 0);
+ const submittedToPersonalDetail = getPersonalDetailsForAccountID(PolicyUtils.getSubmitToAccountID(policy, report?.ownerAccountID ?? 0));
let submittedToDisplayName = `${submittedToPersonalDetail.displayName ?? ''}${
submittedToPersonalDetail.displayName !== submittedToPersonalDetail.login ? ` (${submittedToPersonalDetail.login})` : ''
}`;
@@ -6373,9 +6372,9 @@ function hasHeldExpenses(iouReportID?: string): boolean {
/**
* Check if all expenses in the Report are on hold
*/
-function hasOnlyHeldExpenses(iouReportID: string): boolean {
- const transactions = TransactionUtils.getAllReportTransactions(iouReportID);
- return !transactions.some((transaction) => !TransactionUtils.isOnHold(transaction));
+function hasOnlyHeldExpenses(iouReportID: string, transactions?: OnyxCollection): boolean {
+ const reportTransactions = TransactionUtils.getAllReportTransactions(iouReportID, transactions);
+ return !reportTransactions.some((transaction) => !TransactionUtils.isOnHold(transaction));
}
/**
diff --git a/src/libs/SearchUtils.ts b/src/libs/SearchUtils.ts
index c1f4de2e5e35..b63147b81136 100644
--- a/src/libs/SearchUtils.ts
+++ b/src/libs/SearchUtils.ts
@@ -5,10 +5,10 @@ import type {ReportListItemType, TransactionListItemType} from '@components/Sele
import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
import type * as OnyxTypes from '@src/types/onyx';
-import type {SearchAccountDetails, SearchDataTypes, SearchTypeToItemMap, SectionsType} from '@src/types/onyx/SearchResults';
+import type {SearchAccountDetails, SearchDataTypes, SearchPersonalDetails, SearchTransaction, SearchTypeToItemMap, SectionsType} from '@src/types/onyx/SearchResults';
import getTopmostCentralPaneRoute from './Navigation/getTopmostCentralPaneRoute';
import navigationRef from './Navigation/navigationRef';
-import type {RootStackParamList, State} from './Navigation/types';
+import type {CentralPaneNavigatorParamList, RootStackParamList, State} from './Navigation/types';
import * as TransactionUtils from './TransactionUtils';
import * as UserUtils from './UserUtils';
@@ -21,7 +21,7 @@ const columnNamesToSortingProperty = {
[CONST.SEARCH_TABLE_COLUMNS.DATE]: 'date' as const,
[CONST.SEARCH_TABLE_COLUMNS.TAG]: 'tag' as const,
[CONST.SEARCH_TABLE_COLUMNS.MERCHANT]: 'formattedMerchant' as const,
- [CONST.SEARCH_TABLE_COLUMNS.TOTAL]: 'formattedTotal' as const,
+ [CONST.SEARCH_TABLE_COLUMNS.TOTAL_AMOUNT]: 'formattedTotal' as const,
[CONST.SEARCH_TABLE_COLUMNS.CATEGORY]: 'category' as const,
[CONST.SEARCH_TABLE_COLUMNS.TYPE]: 'type' as const,
[CONST.SEARCH_TABLE_COLUMNS.ACTION]: 'action' as const,
@@ -30,6 +30,32 @@ const columnNamesToSortingProperty = {
[CONST.SEARCH_TABLE_COLUMNS.RECEIPT]: null,
};
+/**
+ * @private
+ */
+function getTransactionItemCommonFormattedProperties(
+ transactionItem: SearchTransaction,
+ from: SearchPersonalDetails,
+ to: SearchAccountDetails,
+): Pick {
+ const isExpenseReport = transactionItem.reportType === CONST.REPORT.TYPE.EXPENSE;
+
+ const formattedFrom = from?.displayName ?? from?.login ?? '';
+ const formattedTo = to?.name ?? to?.displayName ?? to?.login ?? '';
+ const formattedTotal = TransactionUtils.getAmount(transactionItem, isExpenseReport);
+ const date = transactionItem?.modifiedCreated ? transactionItem.modifiedCreated : transactionItem?.created;
+ const merchant = TransactionUtils.getMerchant(transactionItem);
+ const formattedMerchant = merchant === CONST.TRANSACTION.PARTIAL_TRANSACTION_MERCHANT || merchant === CONST.TRANSACTION.DEFAULT_MERCHANT ? '' : merchant;
+
+ return {
+ formattedFrom,
+ formattedTo,
+ date,
+ formattedTotal,
+ formattedMerchant,
+ };
+}
+
function isSearchDataType(type: string): type is SearchDataTypes {
const searchDataTypes: string[] = Object.values(CONST.SEARCH_DATA_TYPES);
return searchDataTypes.includes(type);
@@ -69,12 +95,7 @@ function getTransactionsSections(data: OnyxTypes.SearchResults['data']): Transac
? (data[`${ONYXKEYS.COLLECTION.POLICY}${transactionItem.policyID}`] as SearchAccountDetails)
: (data.personalDetailsList?.[transactionItem.managerID] as SearchAccountDetails);
- const formattedFrom = from.displayName ?? from.login ?? '';
- const formattedTo = to?.name ?? to?.displayName ?? to?.login ?? '';
- const formattedTotal = TransactionUtils.getAmount(transactionItem, isExpenseReport);
- const date = transactionItem?.modifiedCreated ? transactionItem.modifiedCreated : transactionItem?.created;
- const merchant = TransactionUtils.getMerchant(transactionItem);
- const formattedMerchant = merchant === CONST.TRANSACTION.PARTIAL_TRANSACTION_MERCHANT || merchant === CONST.TRANSACTION.DEFAULT_MERCHANT ? '' : merchant;
+ const {formattedFrom, formattedTo, formattedTotal, formattedMerchant, date} = getTransactionItemCommonFormattedProperties(transactionItem, from, to);
return {
...transactionItem,
@@ -82,9 +103,9 @@ function getTransactionsSections(data: OnyxTypes.SearchResults['data']): Transac
to,
formattedFrom,
formattedTo,
- date,
formattedTotal,
formattedMerchant,
+ date,
shouldShowMerchant,
shouldShowCategory,
shouldShowTag,
@@ -119,12 +140,7 @@ function getReportSections(data: OnyxTypes.SearchResults['data']): ReportListIte
? (data[`${ONYXKEYS.COLLECTION.POLICY}${transactionItem.policyID}`] as SearchAccountDetails)
: (data.personalDetailsList?.[transactionItem.managerID] as SearchAccountDetails);
- const formattedFrom = from.displayName ?? from.login ?? '';
- const formattedTo = to?.name ?? to?.displayName ?? to?.login ?? '';
- const formattedTotal = TransactionUtils.getAmount(transactionItem, isExpenseReport);
- const date = transactionItem?.modifiedCreated ? transactionItem.modifiedCreated : transactionItem?.created;
- const merchant = TransactionUtils.getMerchant(transactionItem);
- const formattedMerchant = merchant === CONST.TRANSACTION.PARTIAL_TRANSACTION_MERCHANT || merchant === CONST.TRANSACTION.DEFAULT_MERCHANT ? '' : merchant;
+ const {formattedFrom, formattedTo, formattedTotal, formattedMerchant, date} = getTransactionItemCommonFormattedProperties(transactionItem, from, to);
const transaction = {
...transactionItem,
@@ -133,8 +149,8 @@ function getReportSections(data: OnyxTypes.SearchResults['data']): ReportListIte
formattedFrom,
formattedTo,
formattedTotal,
- date,
formattedMerchant,
+ date,
shouldShowMerchant,
shouldShowCategory,
shouldShowTag,
@@ -221,7 +237,7 @@ function getSortedTransactionData(data: TransactionListItemType[], sortBy?: Sear
function getSearchParams() {
const topmostCentralPaneRoute = getTopmostCentralPaneRoute(navigationRef.getRootState() as State);
- return topmostCentralPaneRoute?.params;
+ return topmostCentralPaneRoute?.params as CentralPaneNavigatorParamList['Search_Central_Pane'];
}
export {getListItem, getQueryHash, getSections, getSortedSections, getShouldShowColumn, getShouldShowMerchant, getSearchType, getSearchParams};
diff --git a/src/libs/SelectionScraper/index.ts b/src/libs/SelectionScraper/index.ts
index eda38a43507c..88726aa633b6 100644
--- a/src/libs/SelectionScraper/index.ts
+++ b/src/libs/SelectionScraper/index.ts
@@ -1,7 +1,7 @@
import render from 'dom-serializer';
import type {Node} from 'domhandler';
import {DataNode, Element} from 'domhandler';
-import Str from 'expensify-common/lib/str';
+import {Str} from 'expensify-common';
import {parseDocument} from 'htmlparser2';
import CONST from '@src/CONST';
import type GetCurrentSelection from './types';
diff --git a/src/libs/SidebarUtils.ts b/src/libs/SidebarUtils.ts
index 651b3185f9af..2d77d66b5f4a 100644
--- a/src/libs/SidebarUtils.ts
+++ b/src/libs/SidebarUtils.ts
@@ -1,4 +1,4 @@
-import Str from 'expensify-common/lib/str';
+import {Str} from 'expensify-common';
import type {OnyxCollection, OnyxEntry} from 'react-native-onyx';
import Onyx from 'react-native-onyx';
import type {ChatReportSelector, PolicySelector, ReportActionsSelector} from '@hooks/useReportIDs';
@@ -80,40 +80,49 @@ function getOrderedReportIDs(
const allReportsDictValues = Object.values(allReports ?? {});
// Filter out all the reports that shouldn't be displayed
- let reportsToDisplay = allReportsDictValues.filter((report) => {
+ let reportsToDisplay: Array = [];
+
+ allReportsDictValues.forEach((report) => {
if (!report) {
- return false;
+ return;
}
- const parentReportActionsKey = `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${report.parentReportID}`;
- const parentReportActions = allReportActions?.[parentReportActionsKey];
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) &&
- !!parentReportAction &&
- ReportUtils.doesTransactionThreadHaveViolations(report, transactionViolations, parentReportAction as OnyxEntry)
- );
+ const doesReportHaveViolations = OptionsListUtils.shouldShowViolations(report, betas ?? [], transactionViolations);
const isHidden = report.notificationPreference === CONST.REPORT.NOTIFICATION_PREFERENCE.HIDDEN;
const isFocused = report.reportID === currentReportId;
const allReportErrors = OptionsListUtils.getAllReportErrors(report, reportActions) ?? {};
const hasErrorsOtherThanFailedReceipt = doesReportHaveViolations || Object.values(allReportErrors).some((error) => error?.[0] !== 'report.genericSmartscanFailureMessage');
+
+ if (hasErrorsOtherThanFailedReceipt) {
+ reportsToDisplay.push({
+ ...report,
+ hasErrorsOtherThanFailedReceipt: true,
+ });
+ return;
+ }
+
const isSystemChat = ReportUtils.isSystemChat(report);
const shouldOverrideHidden = hasErrorsOtherThanFailedReceipt || isFocused || isSystemChat || report.isPinned;
+
if (isHidden && !shouldOverrideHidden) {
- return false;
+ return;
}
- return ReportUtils.shouldReportBeInOptionList({
- report,
- currentReportId: currentReportId ?? '',
- isInFocusMode,
- betas,
- policies: policies as OnyxCollection,
- excludeEmptyChats: true,
- doesReportHaveViolations,
- includeSelfDM: true,
- });
+ if (
+ ReportUtils.shouldReportBeInOptionList({
+ report,
+ currentReportId: currentReportId ?? '',
+ isInFocusMode,
+ betas,
+ policies: policies as OnyxCollection,
+ excludeEmptyChats: true,
+ doesReportHaveViolations,
+ includeSelfDM: true,
+ })
+ ) {
+ reportsToDisplay.push(report);
+ }
});
// The LHN is split into four distinct groups, and each group is sorted a little differently. The groups will ALWAYS be in this order:
@@ -129,6 +138,7 @@ function getOrderedReportIDs(
const draftReports: Array> = [];
const nonArchivedReports: Array> = [];
const archivedReports: Array> = [];
+ const errorReports: Array> = [];
if (currentPolicyID || policyMemberAccountIDs.length > 0) {
reportsToDisplay = reportsToDisplay.filter(
@@ -137,7 +147,7 @@ function getOrderedReportIDs(
}
// There are a few properties that need to be calculated for the report which are used when sorting reports.
reportsToDisplay.forEach((reportToDisplay) => {
- let report = reportToDisplay as OnyxEntry;
+ let report = reportToDisplay;
if (report) {
report = {
...report,
@@ -153,6 +163,8 @@ function getOrderedReportIDs(
draftReports.push(report);
} else if (ReportUtils.isArchivedRoom(report)) {
archivedReports.push(report);
+ } else if (report?.hasErrorsOtherThanFailedReceipt) {
+ errorReports.push(report);
} else {
nonArchivedReports.push(report);
}
@@ -160,6 +172,8 @@ function getOrderedReportIDs(
// Sort each group of reports accordingly
pinnedAndGBRReports.sort((a, b) => (a?.displayName && b?.displayName ? localeCompare(a.displayName, b.displayName) : 0));
+ errorReports.sort((a, b) => (a?.displayName && b?.displayName ? localeCompare(a.displayName, b.displayName) : 0));
+
draftReports.sort((a, b) => (a?.displayName && b?.displayName ? localeCompare(a.displayName, b.displayName) : 0));
if (isInDefaultMode) {
@@ -180,7 +194,7 @@ function getOrderedReportIDs(
// Now that we have all the reports grouped and sorted, they must be flattened into an array and only return the reportID.
// The order the arrays are concatenated in matters and will determine the order that the groups are displayed in the sidebar.
- const LHNReports = [...pinnedAndGBRReports, ...draftReports, ...nonArchivedReports, ...archivedReports].map((report) => report?.reportID ?? '');
+ const LHNReports = [...pinnedAndGBRReports, ...errorReports, ...draftReports, ...nonArchivedReports, ...archivedReports].map((report) => report?.reportID ?? '');
return LHNReports;
}
diff --git a/src/libs/TransactionUtils.ts b/src/libs/TransactionUtils.ts
index 886520773f35..b230d07715a4 100644
--- a/src/libs/TransactionUtils.ts
+++ b/src/libs/TransactionUtils.ts
@@ -58,7 +58,7 @@ function isScanRequest(transaction: OnyxEntry): boolean {
return transaction?.iouRequestType === CONST.IOU.REQUEST_TYPE.SCAN;
}
- return Boolean(transaction?.receipt?.source);
+ return Boolean(transaction?.receipt?.source) && transaction?.amount === 0;
}
function getRequestType(transaction: OnyxEntry): IOURequestType {
@@ -553,12 +553,12 @@ function hasRoute(transaction: OnyxEntry, isDistanceRequestType: bo
return !!transaction?.routes?.route0?.geometry?.coordinates || (isDistanceRequestType && !!transaction?.comment?.customUnit?.quantity);
}
-function getAllReportTransactions(reportID?: string): Transaction[] {
+function getAllReportTransactions(reportID?: string, transactions?: OnyxCollection): Transaction[] {
// `reportID` from the `/CreateDistanceRequest` endpoint return's number instead of string for created `transaction`.
// For reference, https://github.com/Expensify/App/pull/26536#issuecomment-1703573277.
// We will update this in a follow-up Issue. According to this comment: https://github.com/Expensify/App/pull/26536#issuecomment-1703591019.
- const transactions: Transaction[] = Object.values(allTransactions ?? {}).filter((transaction): transaction is Transaction => transaction !== null);
- return transactions.filter((transaction) => `${transaction.reportID}` === `${reportID}`);
+ const nonNullableTransactions: Transaction[] = Object.values(transactions ?? allTransactions ?? {}).filter((transaction): transaction is Transaction => transaction !== null);
+ return nonNullableTransactions.filter((transaction) => `${transaction.reportID}` === `${reportID}`);
}
function waypointHasValidAddress(waypoint: RecentWaypoint | Waypoint): boolean {
diff --git a/src/libs/UserUtils.ts b/src/libs/UserUtils.ts
index 2acebd4636f5..946c92fed19d 100644
--- a/src/libs/UserUtils.ts
+++ b/src/libs/UserUtils.ts
@@ -1,4 +1,4 @@
-import Str from 'expensify-common/lib/str';
+import {Str} from 'expensify-common';
import type {OnyxEntry} from 'react-native-onyx';
import type {ValueOf} from 'type-fest';
import * as defaultAvatars from '@components/Icon/DefaultAvatars';
diff --git a/src/libs/ValidationUtils.ts b/src/libs/ValidationUtils.ts
index 19e35e236e24..33afff36c7a6 100644
--- a/src/libs/ValidationUtils.ts
+++ b/src/libs/ValidationUtils.ts
@@ -1,6 +1,5 @@
import {addYears, endOfMonth, format, isAfter, isBefore, isSameDay, isValid, isWithinInterval, parse, parseISO, startOfDay, subYears} from 'date-fns';
-import Str from 'expensify-common/lib/str';
-import {URL_REGEX_WITH_REQUIRED_PROTOCOL} from 'expensify-common/lib/Url';
+import {Str, Url} from 'expensify-common';
import isDate from 'lodash/isDate';
import isEmpty from 'lodash/isEmpty';
import isObject from 'lodash/isObject';
@@ -241,7 +240,7 @@ function getDatePassedError(inputDate: string): string {
*/
function isValidWebsite(url: string): boolean {
const isLowerCase = url === url.toLowerCase();
- return new RegExp(`^${URL_REGEX_WITH_REQUIRED_PROTOCOL}$`, 'i').test(url) && isLowerCase;
+ return new RegExp(`^${Url.URL_REGEX_WITH_REQUIRED_PROTOCOL}$`, 'i').test(url) && isLowerCase;
}
function validateIdentity(identity: Record): Record {
diff --git a/src/libs/actions/App.ts b/src/libs/actions/App.ts
index bc6ee1f592e5..ae7fc115ac22 100644
--- a/src/libs/actions/App.ts
+++ b/src/libs/actions/App.ts
@@ -1,5 +1,5 @@
// Issue - https://github.com/Expensify/App/issues/26719
-import Str from 'expensify-common/lib/str';
+import {Str} from 'expensify-common';
import type {AppStateStatus} from 'react-native';
import {AppState} from 'react-native';
import type {OnyxCollection, OnyxEntry, OnyxUpdate} from 'react-native-onyx';
diff --git a/src/libs/actions/Device/generateDeviceID/index.android.ts b/src/libs/actions/Device/generateDeviceID/index.android.ts
index d662967a9f76..17563e039e6a 100644
--- a/src/libs/actions/Device/generateDeviceID/index.android.ts
+++ b/src/libs/actions/Device/generateDeviceID/index.android.ts
@@ -1,4 +1,4 @@
-import Str from 'expensify-common/lib/str';
+import {Str} from 'expensify-common';
import DeviceInfo from 'react-native-device-info';
import type GenerateDeviceID from './types';
diff --git a/src/libs/actions/Device/generateDeviceID/index.ts b/src/libs/actions/Device/generateDeviceID/index.ts
index 82ea72ba8180..a88457463f34 100644
--- a/src/libs/actions/Device/generateDeviceID/index.ts
+++ b/src/libs/actions/Device/generateDeviceID/index.ts
@@ -1,4 +1,4 @@
-import Str from 'expensify-common/lib/str';
+import {Str} from 'expensify-common';
import type GenerateDeviceID from './types';
const uniqueID = Str.guid();
diff --git a/src/libs/actions/Device/getDeviceInfo/getOSAndName/index.native.ts b/src/libs/actions/Device/getDeviceInfo/getOSAndName/index.native.ts
index 16a48348df71..06c425438053 100644
--- a/src/libs/actions/Device/getDeviceInfo/getOSAndName/index.native.ts
+++ b/src/libs/actions/Device/getDeviceInfo/getOSAndName/index.native.ts
@@ -1,4 +1,4 @@
-import Str from 'expensify-common/lib/str';
+import {Str} from 'expensify-common';
import RNDeviceInfo from 'react-native-device-info';
import type {GetOSAndName} from './types';
diff --git a/src/libs/actions/Device/getDeviceInfo/getOSAndName/index.ts b/src/libs/actions/Device/getDeviceInfo/getOSAndName/index.ts
index 29b004412f64..7111cab41ad1 100644
--- a/src/libs/actions/Device/getDeviceInfo/getOSAndName/index.ts
+++ b/src/libs/actions/Device/getDeviceInfo/getOSAndName/index.ts
@@ -1,4 +1,3 @@
-// Don't import this file with '* as Device'. It's known to make VSCode IntelliSense crash.
-import {getOSAndName} from 'expensify-common/lib/Device';
+import {Device} from 'expensify-common';
-export default getOSAndName;
+export default Device.getOSAndName;
diff --git a/src/libs/actions/IOU.ts b/src/libs/actions/IOU.ts
index 014ed5da3d48..5f8008e16fb2 100644
--- a/src/libs/actions/IOU.ts
+++ b/src/libs/actions/IOU.ts
@@ -1,6 +1,5 @@
import {format} from 'date-fns';
-import fastMerge from 'expensify-common/lib/fastMerge';
-import Str from 'expensify-common/lib/str';
+import {fastMerge, Str} from 'expensify-common';
import type {OnyxCollection, OnyxEntry, OnyxUpdate} from 'react-native-onyx';
import Onyx from 'react-native-onyx';
import type {ValueOf} from 'type-fest';
@@ -2253,9 +2252,9 @@ function getTrackExpenseInformation(
createdReportActionIDForThread: optimisticCreatedActionForTransactionThread.reportActionID,
actionableWhisperReportActionIDParam: actionableTrackExpenseWhisper?.reportActionID ?? '',
onyxData: {
- optimisticData: [...optimisticData, ...trackExpenseOnyxData[0]],
- successData: [...successData, ...trackExpenseOnyxData[1]],
- failureData: [...failureData, ...trackExpenseOnyxData[2]],
+ optimisticData: optimisticData.concat(trackExpenseOnyxData[0]),
+ successData: successData.concat(trackExpenseOnyxData[1]),
+ failureData: failureData.concat(trackExpenseOnyxData[2]),
},
};
}
@@ -3514,7 +3513,7 @@ function sendInvoice(
const {senderWorkspaceID, receiver, invoiceRoom, createdChatReportActionID, invoiceReportID, reportPreviewReportActionID, transactionID, transactionThreadReportID, onyxData} =
getSendInvoiceInformation(transaction, currentUserAccountID, invoiceChatReport, receiptFile, policy, policyTagList, policyCategories);
- let parameters: SendInvoiceParams = {
+ const parameters: SendInvoiceParams = {
senderWorkspaceID,
accountID: currentUserAccountID,
amount: transaction?.amount ?? 0,
@@ -3529,20 +3528,9 @@ function sendInvoice(
reportPreviewReportActionID,
transactionID,
transactionThreadReportID,
+ ...(invoiceChatReport?.reportID ? {receiverInvoiceRoomID: invoiceChatReport.reportID} : {receiverEmail: receiver.login ?? ''}),
};
- if (invoiceChatReport) {
- parameters = {
- ...parameters,
- receiverInvoiceRoomID: invoiceChatReport.reportID,
- };
- } else {
- parameters = {
- ...parameters,
- receiverEmail: receiver.login,
- };
- }
-
API.write(WRITE_COMMANDS.SEND_INVOICE, parameters, onyxData);
Navigation.dismissModalWithReport(invoiceRoom);
diff --git a/src/libs/actions/PaymentMethods.ts b/src/libs/actions/PaymentMethods.ts
index c5a74bdc6ace..c12f7a042659 100644
--- a/src/libs/actions/PaymentMethods.ts
+++ b/src/libs/actions/PaymentMethods.ts
@@ -3,6 +3,7 @@ import type {MutableRefObject} from 'react';
import type {GestureResponderEvent} from 'react-native';
import type {OnyxEntry, OnyxUpdate} from 'react-native-onyx';
import Onyx from 'react-native-onyx';
+import type {ValueOf} from 'type-fest';
import * as API from '@libs/API';
import type {AddPaymentCardParams, DeletePaymentCardParams, MakeDefaultPaymentMethodParams, PaymentCardParams, TransferWalletBalanceParams} from '@libs/API/parameters';
import {READ_COMMANDS, WRITE_COMMANDS} from '@libs/API/types';
@@ -198,6 +199,64 @@ function addPaymentCard(params: PaymentCardParams) {
});
}
+/**
+ * Calls the API to add a new card.
+ *
+ */
+function addSubscriptionPaymentCard(cardData: {
+ cardNumber: string;
+ cardYear: string;
+ cardMonth: string;
+ cardCVV: string;
+ addressName: string;
+ addressZip: string;
+ currency: ValueOf;
+}) {
+ const {cardNumber, cardYear, cardMonth, cardCVV, addressName, addressZip, currency} = cardData;
+
+ const parameters: AddPaymentCardParams = {
+ cardNumber,
+ cardYear,
+ cardMonth,
+ cardCVV,
+ addressName,
+ addressZip,
+ currency,
+ isP2PDebitCard: false,
+ };
+
+ const optimisticData: OnyxUpdate[] = [
+ {
+ onyxMethod: Onyx.METHOD.MERGE,
+ key: ONYXKEYS.FORMS.ADD_DEBIT_CARD_FORM,
+ value: {isLoading: true},
+ },
+ ];
+
+ const successData: OnyxUpdate[] = [
+ {
+ onyxMethod: Onyx.METHOD.MERGE,
+ key: ONYXKEYS.FORMS.ADD_DEBIT_CARD_FORM,
+ value: {isLoading: false},
+ },
+ ];
+
+ const failureData: OnyxUpdate[] = [
+ {
+ onyxMethod: Onyx.METHOD.MERGE,
+ key: ONYXKEYS.FORMS.ADD_DEBIT_CARD_FORM,
+ value: {isLoading: false},
+ },
+ ];
+
+ // TODO integrate API for subscription card as a follow up
+ API.write(WRITE_COMMANDS.ADD_PAYMENT_CARD, parameters, {
+ optimisticData,
+ successData,
+ failureData,
+ });
+}
+
/**
* Resets the values for the add debit card form back to their initial states
*/
@@ -373,6 +432,7 @@ export {
makeDefaultPaymentMethod,
kycWallRef,
continueSetup,
+ addSubscriptionPaymentCard,
clearDebitCardFormErrorAndSubmit,
dismissSuccessfulTransferBalancePage,
transferWalletBalance,
diff --git a/src/libs/actions/Policy/DistanceRate.ts b/src/libs/actions/Policy/DistanceRate.ts
index f48c3e9033ac..6b0a9c5c3ee0 100644
--- a/src/libs/actions/Policy/DistanceRate.ts
+++ b/src/libs/actions/Policy/DistanceRate.ts
@@ -520,6 +520,148 @@ function deletePolicyDistanceRates(policyID: string, customUnit: CustomUnit, rat
API.write(WRITE_COMMANDS.DELETE_POLICY_DISTANCE_RATES, params, {optimisticData, successData, failureData});
}
+function updateDistanceTaxClaimableValue(policyID: string, customUnit: CustomUnit, customUnitRates: Rate[]) {
+ const currentRates = customUnit.rates;
+ const optimisticRates: Record = {};
+ const successRates: Record = {};
+ const failureRates: Record = {};
+ const rateIDs = customUnitRates.map((rate) => rate.customUnitRateID);
+
+ for (const rateID of Object.keys(customUnit.rates)) {
+ if (rateIDs.includes(rateID)) {
+ const foundRate = customUnitRates.find((rate) => rate.customUnitRateID === rateID);
+ optimisticRates[rateID] = {...foundRate, pendingFields: {rate: CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE}};
+ successRates[rateID] = {...foundRate, pendingFields: {rate: null}};
+ failureRates[rateID] = {
+ ...currentRates[rateID],
+ pendingFields: {rate: null},
+ errorFields: {rate: ErrorUtils.getMicroSecondOnyxError('common.genericErrorMessage')},
+ };
+ }
+ }
+
+ const optimisticData: OnyxUpdate[] = [
+ {
+ onyxMethod: Onyx.METHOD.MERGE,
+ key: `${ONYXKEYS.COLLECTION.POLICY}${policyID}`,
+ value: {
+ customUnits: {
+ [customUnit.customUnitID]: {
+ rates: optimisticRates,
+ },
+ },
+ },
+ },
+ ];
+
+ const successData: OnyxUpdate[] = [
+ {
+ onyxMethod: Onyx.METHOD.MERGE,
+ key: `${ONYXKEYS.COLLECTION.POLICY}${policyID}`,
+ value: {
+ customUnits: {
+ [customUnit.customUnitID]: {
+ rates: successRates,
+ },
+ },
+ },
+ },
+ ];
+
+ const failureData: OnyxUpdate[] = [
+ {
+ onyxMethod: Onyx.METHOD.MERGE,
+ key: `${ONYXKEYS.COLLECTION.POLICY}${policyID}`,
+ value: {
+ customUnits: {
+ [customUnit.customUnitID]: {
+ rates: failureRates,
+ },
+ },
+ },
+ },
+ ];
+
+ const params: UpdatePolicyDistanceRateValueParams = {
+ policyID,
+ customUnitID: customUnit.customUnitID,
+ customUnitRateArray: JSON.stringify(prepareCustomUnitRatesArray(customUnitRates)),
+ };
+
+ API.write(WRITE_COMMANDS.UPDATE_DISTANCE_TAX_CLAIMABLE_VALUE, params, {optimisticData, successData, failureData});
+}
+
+function updateDistanceTaxRate(policyID: string, customUnit: CustomUnit, customUnitRates: Rate[]) {
+ const currentRates = customUnit.rates;
+ const optimisticRates: Record = {};
+ const successRates: Record = {};
+ const failureRates: Record = {};
+ const rateIDs = customUnitRates.map((rate) => rate.customUnitRateID);
+
+ for (const rateID of Object.keys(customUnit.rates)) {
+ if (rateIDs.includes(rateID)) {
+ const foundRate = customUnitRates.find((rate) => rate.customUnitRateID === rateID);
+ optimisticRates[rateID] = {...foundRate, pendingFields: {rate: CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE}};
+ successRates[rateID] = {...foundRate, pendingFields: {rate: null}};
+ failureRates[rateID] = {
+ ...currentRates[rateID],
+ pendingFields: {rate: null},
+ errorFields: {rate: ErrorUtils.getMicroSecondOnyxError('common.genericErrorMessage')},
+ };
+ }
+ }
+
+ const optimisticData: OnyxUpdate[] = [
+ {
+ onyxMethod: Onyx.METHOD.MERGE,
+ key: `${ONYXKEYS.COLLECTION.POLICY}${policyID}`,
+ value: {
+ customUnits: {
+ [customUnit.customUnitID]: {
+ rates: optimisticRates,
+ },
+ },
+ },
+ },
+ ];
+
+ const successData: OnyxUpdate[] = [
+ {
+ onyxMethod: Onyx.METHOD.MERGE,
+ key: `${ONYXKEYS.COLLECTION.POLICY}${policyID}`,
+ value: {
+ customUnits: {
+ [customUnit.customUnitID]: {
+ rates: successRates,
+ },
+ },
+ },
+ },
+ ];
+
+ const failureData: OnyxUpdate[] = [
+ {
+ onyxMethod: Onyx.METHOD.MERGE,
+ key: `${ONYXKEYS.COLLECTION.POLICY}${policyID}`,
+ value: {
+ customUnits: {
+ [customUnit.customUnitID]: {
+ rates: failureRates,
+ },
+ },
+ },
+ },
+ ];
+
+ const params: UpdatePolicyDistanceRateValueParams = {
+ policyID,
+ customUnitID: customUnit.customUnitID,
+ customUnitRateArray: JSON.stringify(prepareCustomUnitRatesArray(customUnitRates)),
+ };
+
+ API.write(WRITE_COMMANDS.UPDATE_POLICY_DISTANCE_TAX_RATE_VALUE, params, {optimisticData, successData, failureData});
+}
+
export {
enablePolicyDistanceRates,
openPolicyDistanceRatesPage,
@@ -532,6 +674,8 @@ export {
updatePolicyDistanceRateValue,
setPolicyDistanceRatesEnabled,
deletePolicyDistanceRates,
+ updateDistanceTaxClaimableValue,
+ updateDistanceTaxRate,
};
export type {NewCustomUnit};
diff --git a/src/libs/actions/Policy/Policy.ts b/src/libs/actions/Policy/Policy.ts
index 2625e7d60dff..a44a989ac91a 100644
--- a/src/libs/actions/Policy/Policy.ts
+++ b/src/libs/actions/Policy/Policy.ts
@@ -1,6 +1,4 @@
-import {PUBLIC_DOMAINS} from 'expensify-common/lib/CONST';
-import ExpensiMark from 'expensify-common/lib/ExpensiMark';
-import Str from 'expensify-common/lib/str';
+import {ExpensiMark, PUBLIC_DOMAINS, Str} from 'expensify-common';
import {escapeRegExp} from 'lodash';
import lodashClone from 'lodash/clone';
import type {NullishDeep, OnyxCollection, OnyxEntry, OnyxUpdate} from 'react-native-onyx';
@@ -1396,7 +1394,9 @@ function addMembersToWorkspace(invitedEmailsToAccountIDs: InvitedEmailsToAccount
// Convert to object with each key containing the error. We don’t
// need to remove the members since that is handled by onClose of OfflineWithFeedback.
- value: failureMembersState,
+ value: {
+ employeeList: failureMembersState,
+ },
},
...membersChats.onyxFailureData,
...announceRoomMembers.onyxFailureData,
@@ -3500,6 +3500,62 @@ function enablePolicyWorkflows(policyID: string, enabled: boolean) {
}
}
+function enableDistanceRequestTax(policyID: string, customUnitName: string, customUnitID: string, attributes: Attributes) {
+ const policy = getPolicy(policyID);
+ const onyxData: OnyxData = {
+ optimisticData: [
+ {
+ onyxMethod: Onyx.METHOD.MERGE,
+ key: `${ONYXKEYS.COLLECTION.POLICY}${policyID}`,
+ value: {
+ customUnits: {
+ [customUnitID]: {
+ attributes,
+ },
+ },
+ pendingFields: {
+ customUnits: CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE,
+ },
+ },
+ },
+ ],
+ successData: [
+ {
+ onyxMethod: Onyx.METHOD.MERGE,
+ key: `${ONYXKEYS.COLLECTION.POLICY}${policyID}`,
+ value: {
+ pendingFields: {
+ customUnits: null,
+ },
+ },
+ },
+ ],
+ failureData: [
+ {
+ onyxMethod: Onyx.METHOD.MERGE,
+ key: `${ONYXKEYS.COLLECTION.POLICY}${policyID}`,
+ value: {
+ customUnits: {
+ [customUnitID]: {
+ attributes: policy.customUnits ? policy.customUnits[customUnitID].attributes : null,
+ },
+ },
+ },
+ },
+ ],
+ };
+
+ const params = {
+ policyID,
+ customUnit: JSON.stringify({
+ customUnitName,
+ customUnitID,
+ attributes,
+ }),
+ };
+ API.write(WRITE_COMMANDS.ENABLE_DISTANCE_REQUEST_TAX, params, onyxData);
+}
+
function openPolicyMoreFeaturesPage(policyID: string) {
const params: OpenPolicyMoreFeaturesPageParams = {policyID};
@@ -3734,6 +3790,7 @@ export {
enablePolicyReportFields,
enablePolicyTaxes,
enablePolicyWorkflows,
+ enableDistanceRequestTax,
openPolicyMoreFeaturesPage,
generateCustomUnitID,
clearQBOErrorField,
diff --git a/src/libs/actions/Report.ts b/src/libs/actions/Report.ts
index 40f3c93a0492..1262e8af7d44 100644
--- a/src/libs/actions/Report.ts
+++ b/src/libs/actions/Report.ts
@@ -1,6 +1,5 @@
import {format as timezoneFormat, utcToZonedTime} from 'date-fns-tz';
-import ExpensiMark from 'expensify-common/lib/ExpensiMark';
-import Str from 'expensify-common/lib/str';
+import {ExpensiMark, Str} from 'expensify-common';
import isEmpty from 'lodash/isEmpty';
import {DeviceEventEmitter, InteractionManager, Linking} from 'react-native';
import type {NullishDeep, OnyxCollection, OnyxEntry, OnyxUpdate} from 'react-native-onyx';
diff --git a/src/libs/actions/connections/index.ts b/src/libs/actions/connections/index.ts
index 185cd089d1e9..45a4e4f6819c 100644
--- a/src/libs/actions/connections/index.ts
+++ b/src/libs/actions/connections/index.ts
@@ -137,6 +137,7 @@ function syncConnection(policyID: string, connectionName: PolicyConnectionName |
value: {
stageInProgress: isQBOConnection ? CONST.POLICY.CONNECTIONS.SYNC_STAGE_NAME.STARTING_IMPORT_QBO : CONST.POLICY.CONNECTIONS.SYNC_STAGE_NAME.STARTING_IMPORT_XERO,
connectionName,
+ timestamp: new Date().toISOString(),
},
},
];
diff --git a/src/libs/fileDownload/FileUtils.ts b/src/libs/fileDownload/FileUtils.ts
index 6517a7a28642..f058ce0f80d8 100644
--- a/src/libs/fileDownload/FileUtils.ts
+++ b/src/libs/fileDownload/FileUtils.ts
@@ -1,4 +1,4 @@
-import Str from 'expensify-common/lib/str';
+import {Str} from 'expensify-common';
import {Alert, Linking, Platform} from 'react-native';
import ImageSize from 'react-native-image-size';
import type {FileObject} from '@components/AttachmentModal';
diff --git a/src/libs/isReportMessageAttachment.ts b/src/libs/isReportMessageAttachment.ts
index 330ba4470097..9ef4125ee8bf 100644
--- a/src/libs/isReportMessageAttachment.ts
+++ b/src/libs/isReportMessageAttachment.ts
@@ -1,4 +1,4 @@
-import Str from 'expensify-common/lib/str';
+import {Str} from 'expensify-common';
import CONST from '@src/CONST';
import type {Message} from '@src/types/onyx/ReportAction';
diff --git a/src/libs/models/BankAccount.ts b/src/libs/models/BankAccount.ts
index 611d77c99927..44b2122d8091 100644
--- a/src/libs/models/BankAccount.ts
+++ b/src/libs/models/BankAccount.ts
@@ -1,4 +1,4 @@
-import Str from 'expensify-common/lib/str';
+import {Str} from 'expensify-common';
import type {ValueOf} from 'type-fest';
import CONST from '@src/CONST';
import type {AdditionalData} from '@src/types/onyx/BankAccount';
diff --git a/src/pages/DetailsPage.tsx b/src/pages/DetailsPage.tsx
index 2198be675c11..c3a8b4fd6711 100755
--- a/src/pages/DetailsPage.tsx
+++ b/src/pages/DetailsPage.tsx
@@ -1,5 +1,5 @@
import type {StackScreenProps} from '@react-navigation/stack';
-import Str from 'expensify-common/lib/str';
+import {Str} from 'expensify-common';
import React from 'react';
import {View} from 'react-native';
import {withOnyx} from 'react-native-onyx';
diff --git a/src/pages/EditReportFieldPage.tsx b/src/pages/EditReportFieldPage.tsx
index 53b8043c9162..5351f00204de 100644
--- a/src/pages/EditReportFieldPage.tsx
+++ b/src/pages/EditReportFieldPage.tsx
@@ -1,4 +1,4 @@
-import Str from 'expensify-common/lib/str';
+import {Str} from 'expensify-common';
import React, {useState} from 'react';
import {withOnyx} from 'react-native-onyx';
import type {OnyxEntry} from 'react-native-onyx';
diff --git a/src/pages/FlagCommentPage.tsx b/src/pages/FlagCommentPage.tsx
index 53aac0ce2a8b..98fe3973ff9d 100644
--- a/src/pages/FlagCommentPage.tsx
+++ b/src/pages/FlagCommentPage.tsx
@@ -19,7 +19,6 @@ import * as ReportUtils from '@libs/ReportUtils';
import * as Report from '@userActions/Report';
import * as Session from '@userActions/Session';
import CONST from '@src/CONST';
-import ROUTES from '@src/ROUTES';
import type SCREENS from '@src/SCREENS';
import type * as OnyxTypes from '@src/types/onyx';
import withReportAndReportActionOrNotFound from './home/report/withReportAndReportActionOrNotFound';
@@ -160,14 +159,7 @@ function FlagCommentPage({parentReportAction, route, report, parentReport, repor
>
{({safeAreaPaddingBottomStyle}) => (
- {
- Navigation.goBack();
- Navigation.navigate(ROUTES.REPORT_WITH_ID.getRoute(report?.reportID ?? ''));
- }}
- />
+
(
+
+ ),
+ [selectedOptions.length, inviteUsers, translate, styles],
+ );
+
return (
-
-
-
-
-
-
+
+
);
}
diff --git a/src/pages/PrivateNotes/PrivateNotesEditPage.tsx b/src/pages/PrivateNotes/PrivateNotesEditPage.tsx
index 19cbe0bbea42..a8c72241c413 100644
--- a/src/pages/PrivateNotes/PrivateNotesEditPage.tsx
+++ b/src/pages/PrivateNotes/PrivateNotesEditPage.tsx
@@ -1,7 +1,6 @@
import {useFocusEffect} from '@react-navigation/native';
import type {StackScreenProps} from '@react-navigation/stack';
-import ExpensiMark from 'expensify-common/lib/ExpensiMark';
-import Str from 'expensify-common/lib/str';
+import {ExpensiMark, Str} from 'expensify-common';
import lodashDebounce from 'lodash/debounce';
import React, {useCallback, useMemo, useRef, useState} from 'react';
import {Keyboard} from 'react-native';
diff --git a/src/pages/ProfilePage.tsx b/src/pages/ProfilePage.tsx
index 6d2e7f526b1e..2000238b6c81 100755
--- a/src/pages/ProfilePage.tsx
+++ b/src/pages/ProfilePage.tsx
@@ -1,5 +1,5 @@
import type {StackScreenProps} from '@react-navigation/stack';
-import Str from 'expensify-common/lib/str';
+import {Str} from 'expensify-common';
import React, {useEffect, useMemo} from 'react';
import {View} from 'react-native';
import type {OnyxEntry} from 'react-native-onyx';
diff --git a/src/pages/ReimbursementAccount/BeneficialOwnersStep.tsx b/src/pages/ReimbursementAccount/BeneficialOwnersStep.tsx
index 393846413a27..2abcc050b749 100644
--- a/src/pages/ReimbursementAccount/BeneficialOwnersStep.tsx
+++ b/src/pages/ReimbursementAccount/BeneficialOwnersStep.tsx
@@ -1,4 +1,4 @@
-import Str from 'expensify-common/lib/str';
+import {Str} from 'expensify-common';
import React, {useState} from 'react';
import {View} from 'react-native';
import type {OnyxEntry} from 'react-native-onyx';
diff --git a/src/pages/ReimbursementAccount/BusinessInfo/substeps/ConfirmationBusiness.tsx b/src/pages/ReimbursementAccount/BusinessInfo/substeps/ConfirmationBusiness.tsx
index 6311a63a4059..4c8331a23d8f 100644
--- a/src/pages/ReimbursementAccount/BusinessInfo/substeps/ConfirmationBusiness.tsx
+++ b/src/pages/ReimbursementAccount/BusinessInfo/substeps/ConfirmationBusiness.tsx
@@ -1,4 +1,4 @@
-import type {CONST as COMMON_CONST} from 'expensify-common/lib/CONST';
+import type {CONST as COMMON_CONST} from 'expensify-common';
import React, {useMemo} from 'react';
import type {OnyxEntry} from 'react-native-onyx';
import {withOnyx} from 'react-native-onyx';
diff --git a/src/pages/ReimbursementAccount/ConnectBankAccount/components/BankAccountValidationForm.tsx b/src/pages/ReimbursementAccount/ConnectBankAccount/components/BankAccountValidationForm.tsx
index a83e18d119c9..9c539cbef6fa 100644
--- a/src/pages/ReimbursementAccount/ConnectBankAccount/components/BankAccountValidationForm.tsx
+++ b/src/pages/ReimbursementAccount/ConnectBankAccount/components/BankAccountValidationForm.tsx
@@ -1,4 +1,4 @@
-import Str from 'expensify-common/lib/str';
+import {Str} from 'expensify-common';
import React, {useCallback} from 'react';
import {View} from 'react-native';
import FormProvider from '@components/Form/FormProvider';
diff --git a/src/pages/ReimbursementAccount/ReimbursementAccountPage.tsx b/src/pages/ReimbursementAccount/ReimbursementAccountPage.tsx
index 8c59e98f7218..dde820368447 100644
--- a/src/pages/ReimbursementAccount/ReimbursementAccountPage.tsx
+++ b/src/pages/ReimbursementAccount/ReimbursementAccountPage.tsx
@@ -1,6 +1,6 @@
import type {RouteProp} from '@react-navigation/native';
import type {StackScreenProps} from '@react-navigation/stack';
-import Str from 'expensify-common/lib/str';
+import {Str} from 'expensify-common';
import lodashPick from 'lodash/pick';
import React, {useEffect, useRef, useState} from 'react';
import {View} from 'react-native';
diff --git a/src/pages/ReportDetailsPage.tsx b/src/pages/ReportDetailsPage.tsx
index 3280aaa4a215..22e2ce618fab 100644
--- a/src/pages/ReportDetailsPage.tsx
+++ b/src/pages/ReportDetailsPage.tsx
@@ -1,4 +1,3 @@
-import {useRoute} from '@react-navigation/native';
import type {StackScreenProps} from '@react-navigation/stack';
import React, {useCallback, useEffect, useMemo, useState} from 'react';
import {View} from 'react-native';
@@ -66,7 +65,6 @@ function ReportDetailsPage({policies, report, session, personalDetails}: ReportD
const {translate} = useLocalize();
const {isOffline} = useNetwork();
const styles = useThemeStyles();
- const route = useRoute();
const [isLastMemberLeavingGroupModalVisible, setIsLastMemberLeavingGroupModalVisible] = useState(false);
const policy = useMemo(() => policies?.[`${ONYXKEYS.COLLECTION.POLICY}${report?.policyID ?? ''}`], [policies, report?.policyID]);
const isPolicyAdmin = useMemo(() => PolicyUtils.isPolicyAdmin(policy ?? null), [policy]);
@@ -312,11 +310,7 @@ function ReportDetailsPage({policies, report, session, personalDetails}: ReportD
return (
-
+
{renderedAvatar}
diff --git a/src/pages/RoomDescriptionPage.tsx b/src/pages/RoomDescriptionPage.tsx
index 3992dff188e2..46a10483fd08 100644
--- a/src/pages/RoomDescriptionPage.tsx
+++ b/src/pages/RoomDescriptionPage.tsx
@@ -1,5 +1,5 @@
import {useFocusEffect} from '@react-navigation/native';
-import ExpensiMark from 'expensify-common/lib/ExpensiMark';
+import {ExpensiMark} from 'expensify-common';
import React, {useCallback, useRef, useState} from 'react';
import {View} from 'react-native';
import type {OnyxCollection} from 'react-native-onyx';
diff --git a/src/pages/RoomInvitePage.tsx b/src/pages/RoomInvitePage.tsx
index 45f85c0cdaa9..46d5d9aed7f4 100644
--- a/src/pages/RoomInvitePage.tsx
+++ b/src/pages/RoomInvitePage.tsx
@@ -1,5 +1,5 @@
import type {StackScreenProps} from '@react-navigation/stack';
-import Str from 'expensify-common/lib/str';
+import {Str} from 'expensify-common';
import React, {useCallback, useEffect, useMemo, useState} from 'react';
import type {SectionListData} from 'react-native';
import {View} from 'react-native';
diff --git a/src/pages/TeachersUnite/IntroSchoolPrincipalPage.tsx b/src/pages/TeachersUnite/IntroSchoolPrincipalPage.tsx
index 6e8c3b340f4e..eabd8642c287 100644
--- a/src/pages/TeachersUnite/IntroSchoolPrincipalPage.tsx
+++ b/src/pages/TeachersUnite/IntroSchoolPrincipalPage.tsx
@@ -1,4 +1,4 @@
-import Str from 'expensify-common/lib/str';
+import {Str} from 'expensify-common';
import React, {useCallback} from 'react';
import {View} from 'react-native';
import {withOnyx} from 'react-native-onyx';
diff --git a/src/pages/TeachersUnite/KnowATeacherPage.tsx b/src/pages/TeachersUnite/KnowATeacherPage.tsx
index cb58009c40f0..aaca679e49b8 100644
--- a/src/pages/TeachersUnite/KnowATeacherPage.tsx
+++ b/src/pages/TeachersUnite/KnowATeacherPage.tsx
@@ -1,4 +1,4 @@
-import Str from 'expensify-common/lib/str';
+import {Str} from 'expensify-common';
import React, {useCallback} from 'react';
import {View} from 'react-native';
import {withOnyx} from 'react-native-onyx';
diff --git a/src/pages/Travel/ManageTrips.tsx b/src/pages/Travel/ManageTrips.tsx
index 73b0121d572a..3779e22ec6ef 100644
--- a/src/pages/Travel/ManageTrips.tsx
+++ b/src/pages/Travel/ManageTrips.tsx
@@ -3,7 +3,6 @@ import {Linking, View} from 'react-native';
import type {FeatureListItem} from '@components/FeatureList';
import FeatureList from '@components/FeatureList';
import * as Illustrations from '@components/Icon/Illustrations';
-import LottieAnimations from '@components/LottieAnimations';
import ScrollView from '@components/ScrollView';
import useLocalize from '@hooks/useLocalize';
import useThemeStyles from '@hooks/useThemeStyles';
@@ -12,7 +11,6 @@ import Navigation from '@libs/Navigation/Navigation';
import colors from '@styles/theme/colors';
import CONST from '@src/CONST';
import ROUTES from '@src/ROUTES';
-import getTripIllustrationStyle from './getTripIllustrationStyle';
const tripsFeatures: FeatureListItem[] = [
{
@@ -29,7 +27,6 @@ function ManageTrips() {
const styles = useThemeStyles();
const {isSmallScreenWidth} = useWindowDimensions();
const {translate} = useLocalize();
- const illustrationStyle = getTripIllustrationStyle();
const navigateToBookTravelDemo = () => {
Linking.openURL(CONST.BOOK_TRAVEL_DEMO_URL);
@@ -47,11 +44,11 @@ function ManageTrips() {
onCtaPress={() => {
Navigation.navigate(ROUTES.TRAVEL_TCS);
}}
+ illustration={Illustrations.EmptyStateTravel}
+ illustrationStyle={[styles.mv4, styles.tripIllustrationSize]}
secondaryButtonText={translate('travel.bookDemo')}
secondaryButtonAccessibilityLabel={translate('travel.bookDemo')}
onSecondaryButtonPress={navigateToBookTravelDemo}
- illustration={LottieAnimations.Plane}
- illustrationStyle={illustrationStyle}
illustrationBackgroundColor={colors.blue600}
titleStyles={styles.textHeadlineH1}
contentPaddingOnLargeScreens={styles.p5}
diff --git a/src/pages/Travel/getTripIllustrationStyle/index.native.ts b/src/pages/Travel/getTripIllustrationStyle/index.native.ts
deleted file mode 100644
index e5b0a1381d7e..000000000000
--- a/src/pages/Travel/getTripIllustrationStyle/index.native.ts
+++ /dev/null
@@ -1,8 +0,0 @@
-import type {ViewStyle} from 'react-native';
-
-// Styling lottie animations for the ManageTrips component requires different margin values depending on the platform.
-export default function getTripIllustrationStyle(): ViewStyle {
- return {
- marginVertical: 20,
- };
-}
diff --git a/src/pages/Travel/getTripIllustrationStyle/index.ts b/src/pages/Travel/getTripIllustrationStyle/index.ts
deleted file mode 100644
index a2a141022d73..000000000000
--- a/src/pages/Travel/getTripIllustrationStyle/index.ts
+++ /dev/null
@@ -1,9 +0,0 @@
-import type {ViewStyle} from 'react-native';
-
-// Styling lottie animations for the ManageTrips component requires different margin values depending on the platform.
-export default function getTripIllustrationStyle(): ViewStyle {
- return {
- marginTop: 20,
- marginBottom: -20,
- };
-}
diff --git a/src/pages/home/ReportScreen.tsx b/src/pages/home/ReportScreen.tsx
index 5696f0b800f4..414745270b68 100644
--- a/src/pages/home/ReportScreen.tsx
+++ b/src/pages/home/ReportScreen.tsx
@@ -21,6 +21,7 @@ import TaskHeaderActionButton from '@components/TaskHeaderActionButton';
import type {CurrentReportIDContextValue} from '@components/withCurrentReportID';
import withCurrentReportID from '@components/withCurrentReportID';
import useAppFocusEvent from '@hooks/useAppFocusEvent';
+import useDeepCompareRef from '@hooks/useDeepCompareRef';
import useIsReportOpenInRHP from '@hooks/useIsReportOpenInRHP';
import useLocalize from '@hooks/useLocalize';
import useNetwork from '@hooks/useNetwork';
@@ -153,6 +154,7 @@ function ReportScreen({
});
const isLoadingReportOnyx = isLoadingOnyxValue(reportResult);
+ const permissions = useDeepCompareRef(reportOnyx?.permissions);
/**
* Create a lightweight Report so as to keep the re-rendering as light as possible by
@@ -201,7 +203,7 @@ function ReportScreen({
isOptimisticReport: reportOnyx?.isOptimisticReport,
lastMentionedTime: reportOnyx?.lastMentionedTime,
avatarUrl: reportOnyx?.avatarUrl,
- permissions: reportOnyx?.permissions,
+ permissions,
invoiceReceiver: reportOnyx?.invoiceReceiver,
}),
[
@@ -242,7 +244,7 @@ function ReportScreen({
reportOnyx?.isOptimisticReport,
reportOnyx?.lastMentionedTime,
reportOnyx?.avatarUrl,
- reportOnyx?.permissions,
+ permissions,
reportOnyx?.invoiceReceiver,
],
);
@@ -711,6 +713,7 @@ function ReportScreen({
onComposerFocus={() => setIsComposerFocus(true)}
onComposerBlur={() => setIsComposerFocus(false)}
report={report}
+ reportMetadata={reportMetadata}
reportNameValuePairs={reportNameValuePairs}
pendingAction={reportPendingAction}
isComposerFullSize={!!isComposerFullSize}
diff --git a/src/pages/home/report/ContextMenu/ContextMenuActions.tsx b/src/pages/home/report/ContextMenu/ContextMenuActions.tsx
index efcd08c35a00..aa7b6aa3bfb5 100644
--- a/src/pages/home/report/ContextMenu/ContextMenuActions.tsx
+++ b/src/pages/home/report/ContextMenu/ContextMenuActions.tsx
@@ -1,5 +1,4 @@
-import ExpensiMark from 'expensify-common/lib/ExpensiMark';
-import Str from 'expensify-common/lib/str';
+import {ExpensiMark, Str} from 'expensify-common';
import type {MutableRefObject} from 'react';
import React from 'react';
import {InteractionManager} from 'react-native';
diff --git a/src/pages/home/report/ReactionList/BaseReactionList.tsx b/src/pages/home/report/ReactionList/BaseReactionList.tsx
index 23417c1395df..82550f7a4f5f 100755
--- a/src/pages/home/report/ReactionList/BaseReactionList.tsx
+++ b/src/pages/home/report/ReactionList/BaseReactionList.tsx
@@ -1,4 +1,4 @@
-import Str from 'expensify-common/lib/str';
+import {Str} from 'expensify-common';
import React from 'react';
import {FlatList} from 'react-native';
import type {FlatListProps} from 'react-native';
diff --git a/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx b/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx
index cd92f8c75ae1..cb722627dcb8 100644
--- a/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx
+++ b/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx
@@ -1,5 +1,5 @@
import {useIsFocused, useNavigation} from '@react-navigation/native';
-import ExpensiMark from 'expensify-common/lib/ExpensiMark';
+import {ExpensiMark} from 'expensify-common';
import lodashDebounce from 'lodash/debounce';
import type {ForwardedRef, MutableRefObject, RefAttributes, RefObject} from 'react';
import React, {forwardRef, memo, useCallback, useEffect, useImperativeHandle, useMemo, useRef, useState} from 'react';
@@ -46,6 +46,7 @@ import SilentCommentUpdater from '@pages/home/report/ReportActionCompose/SilentC
import Suggestions from '@pages/home/report/ReportActionCompose/Suggestions';
import * as EmojiPickerActions from '@userActions/EmojiPickerAction';
import * as InputFocus from '@userActions/InputFocus';
+import * as Modal from '@userActions/Modal';
import * as Report from '@userActions/Report';
import * as User from '@userActions/User';
import CONST from '@src/CONST';
@@ -285,7 +286,11 @@ function ComposerWithSuggestions(
const parentReportAction = parentReportActions?.[parentReportActionID ?? ''] ?? null;
const shouldAutoFocus =
- !modal?.isVisible && isFocused && (shouldFocusInputOnScreenFocus || (isEmptyChat && !ReportActionsUtils.isTransactionThread(parentReportAction))) && shouldShowComposeInput;
+ !modal?.isVisible &&
+ Modal.areAllModalsHidden() &&
+ isFocused &&
+ (shouldFocusInputOnScreenFocus || (isEmptyChat && !ReportActionsUtils.isTransactionThread(parentReportAction))) &&
+ shouldShowComposeInput;
const valueRef = useRef(value);
valueRef.current = value;
diff --git a/src/pages/home/report/ReportActionCompose/SuggestionMention.tsx b/src/pages/home/report/ReportActionCompose/SuggestionMention.tsx
index a63f7b581286..dcae4b674fc3 100644
--- a/src/pages/home/report/ReportActionCompose/SuggestionMention.tsx
+++ b/src/pages/home/report/ReportActionCompose/SuggestionMention.tsx
@@ -1,4 +1,4 @@
-import Str from 'expensify-common/lib/str';
+import {Str} from 'expensify-common';
import lodashSortBy from 'lodash/sortBy';
import type {ForwardedRef} from 'react';
import React, {forwardRef, useCallback, useEffect, useImperativeHandle, useRef, useState} from 'react';
diff --git a/src/pages/home/report/ReportActionItemBasicMessage.tsx b/src/pages/home/report/ReportActionItemBasicMessage.tsx
index a28f2af24448..20098933b02e 100644
--- a/src/pages/home/report/ReportActionItemBasicMessage.tsx
+++ b/src/pages/home/report/ReportActionItemBasicMessage.tsx
@@ -1,4 +1,4 @@
-import Str from 'expensify-common/lib/str';
+import {Str} from 'expensify-common';
import React from 'react';
import {View} from 'react-native';
import Text from '@components/Text';
diff --git a/src/pages/home/report/ReportActionItemMessageEdit.tsx b/src/pages/home/report/ReportActionItemMessageEdit.tsx
index 6cb03e8dae05..b972ef270aaa 100644
--- a/src/pages/home/report/ReportActionItemMessageEdit.tsx
+++ b/src/pages/home/report/ReportActionItemMessageEdit.tsx
@@ -1,4 +1,4 @@
-import ExpensiMark from 'expensify-common/lib/ExpensiMark';
+import {ExpensiMark} from 'expensify-common';
import lodashDebounce from 'lodash/debounce';
import type {ForwardedRef} from 'react';
import React, {forwardRef, useCallback, useEffect, useMemo, useRef, useState} from 'react';
diff --git a/src/pages/home/report/ReportFooter.tsx b/src/pages/home/report/ReportFooter.tsx
index ac56fe916bc9..decd8f1b8d56 100644
--- a/src/pages/home/report/ReportFooter.tsx
+++ b/src/pages/home/report/ReportFooter.tsx
@@ -40,6 +40,8 @@ type ReportFooterProps = ReportFooterOnyxProps & {
/** Report object for the current report */
report?: OnyxTypes.Report;
+ reportMetadata?: OnyxEntry;
+
reportNameValuePairs?: OnyxEntry;
/** The last report action */
@@ -72,6 +74,7 @@ function ReportFooter({
pendingAction,
session,
report = {reportID: '0'},
+ reportMetadata,
reportNameValuePairs,
shouldShowComposeInput = false,
isEmptyChat = true,
@@ -90,7 +93,10 @@ function ReportFooter({
const isAnonymousUser = session?.authTokenType === CONST.AUTH_TOKEN_TYPES.ANONYMOUS;
const isSmallSizeLayout = windowWidth - (isSmallScreenWidth ? 0 : variables.sideBarWidth) < variables.anonymousReportFooterBreakpoint;
- const hideComposer = !ReportUtils.canUserPerformWriteAction(report, reportNameValuePairs) || blockedFromChat;
+
+ // If a user just signed in and is viewing a public report, optimistically show the composer while loading the report, since they will have write access when the response comes back.
+ const showComposerOptimistically = !isAnonymousUser && ReportUtils.isPublicRoom(report) && reportMetadata?.isLoadingInitialReportActions;
+ const hideComposer = (!ReportUtils.canUserPerformWriteAction(report, reportNameValuePairs) && !showComposerOptimistically) || blockedFromChat;
const canWriteInReport = ReportUtils.canWriteInReport(report);
const isSystemChat = ReportUtils.isSystemChat(report);
@@ -211,6 +217,7 @@ export default withOnyx({
prevProps.lastReportAction === nextProps.lastReportAction &&
prevProps.shouldShowComposeInput === nextProps.shouldShowComposeInput &&
prevProps.isReportReadyForDisplay === nextProps.isReportReadyForDisplay &&
- lodashIsEqual(prevProps.session, nextProps.session),
+ lodashIsEqual(prevProps.session, nextProps.session) &&
+ lodashIsEqual(prevProps.reportMetadata, nextProps.reportMetadata),
),
);
diff --git a/src/pages/home/report/comment/TextCommentFragment.tsx b/src/pages/home/report/comment/TextCommentFragment.tsx
index cf1ab6f8aa19..f29acf8d35fc 100644
--- a/src/pages/home/report/comment/TextCommentFragment.tsx
+++ b/src/pages/home/report/comment/TextCommentFragment.tsx
@@ -1,4 +1,4 @@
-import Str from 'expensify-common/lib/str';
+import {Str} from 'expensify-common';
import {isEmpty} from 'lodash';
import React, {memo} from 'react';
import type {StyleProp, TextStyle} from 'react-native';
diff --git a/src/pages/home/report/comment/shouldRenderAsText/index.native.ts b/src/pages/home/report/comment/shouldRenderAsText/index.native.ts
index 7c5758f8720d..ac68a9e660cf 100644
--- a/src/pages/home/report/comment/shouldRenderAsText/index.native.ts
+++ b/src/pages/home/report/comment/shouldRenderAsText/index.native.ts
@@ -1,4 +1,4 @@
-import Str from 'expensify-common/lib/str';
+import {Str} from 'expensify-common';
/**
* Whether to render the report action as text
diff --git a/src/pages/home/report/comment/shouldRenderAsText/index.ts b/src/pages/home/report/comment/shouldRenderAsText/index.ts
index f26f43c528eb..7e8200671c1a 100644
--- a/src/pages/home/report/comment/shouldRenderAsText/index.ts
+++ b/src/pages/home/report/comment/shouldRenderAsText/index.ts
@@ -1,4 +1,4 @@
-import Str from 'expensify-common/lib/str';
+import {Str} from 'expensify-common';
/**
* Whether to render the report action as text
diff --git a/src/pages/iou/SplitBillDetailsPage.tsx b/src/pages/iou/SplitBillDetailsPage.tsx
index ff8970a3a0a7..80acb7486774 100644
--- a/src/pages/iou/SplitBillDetailsPage.tsx
+++ b/src/pages/iou/SplitBillDetailsPage.tsx
@@ -104,19 +104,20 @@ function SplitBillDetailsPage({personalDetails, report, route, reportActions, tr
{isScanning && (
-
- }
- description={translate('iou.receiptScanInProgressDescription')}
- shouldShowBorderBottom
- shouldStyleFlexGrow={false}
- />
+
+
+ }
+ description={translate('iou.receiptScanInProgressDescription')}
+ shouldStyleFlexGrow={false}
+ />
+
)}
{!!participants.length && (
{
if (personalDetails?.[transaction?.splitPayerAccountIDs?.[0] ?? -1]) {
return personalDetails?.[transaction?.splitPayerAccountIDs?.[0] ?? -1];
@@ -468,7 +469,7 @@ function IOURequestStepConfirmation({
return;
}
- if (requestType === CONST.IOU.REQUEST_TYPE.DISTANCE && !IOUUtils.isMovingTransactionFromTrackExpense(action)) {
+ if (requestType === CONST.IOU.REQUEST_TYPE.DISTANCE && !isMovingTransactionFromTrackExpense) {
const customUnitRateID = TransactionUtils.getRateID(transaction) ?? '';
createDistanceRequest(selectedParticipants, trimmedComment, customUnitRateID);
return;
@@ -489,7 +490,7 @@ function IOURequestStepConfirmation({
createDistanceRequest,
isSharingTrackExpense,
isCategorizingTrackExpense,
- action,
+ isMovingTransactionFromTrackExpense,
policy,
policyTags,
policyCategories,
@@ -538,7 +539,9 @@ function IOURequestStepConfirmation({
diff --git a/src/pages/iou/request/step/IOURequestStepMerchant.tsx b/src/pages/iou/request/step/IOURequestStepMerchant.tsx
index fc7d39b49089..99606eb50e5c 100644
--- a/src/pages/iou/request/step/IOURequestStepMerchant.tsx
+++ b/src/pages/iou/request/step/IOURequestStepMerchant.tsx
@@ -60,10 +60,11 @@ function IOURequestStepMerchant({
// In the split flow, when editing we use SPLIT_TRANSACTION_DRAFT to save draft value
const isEditingSplitBill = iouType === CONST.IOU.TYPE.SPLIT && isEditing;
+ const isTypeInvoice = iouType === CONST.IOU.TYPE.INVOICE;
const merchant = ReportUtils.getTransactionDetails(isEditingSplitBill && !isEmptyObject(splitDraftTransaction) ? splitDraftTransaction : transaction)?.merchant;
const isEmptyMerchant = merchant === '' || merchant === CONST.TRANSACTION.PARTIAL_TRANSACTION_MERCHANT;
- const isMerchantRequired = ReportUtils.isReportInGroupPolicy(report) || transaction?.participants?.some((participant) => Boolean(participant.isPolicyExpenseChat));
+ const isMerchantRequired = ReportUtils.isReportInGroupPolicy(report) || isTypeInvoice || transaction?.participants?.some((participant) => Boolean(participant.isPolicyExpenseChat));
const navigateBack = () => {
Navigation.goBack(backTo);
};
diff --git a/src/pages/iou/request/step/IOURequestStepScan/index.tsx b/src/pages/iou/request/step/IOURequestStepScan/index.tsx
index ede79e009a49..0e74b7c392ae 100644
--- a/src/pages/iou/request/step/IOURequestStepScan/index.tsx
+++ b/src/pages/iou/request/step/IOURequestStepScan/index.tsx
@@ -209,7 +209,7 @@ function IOURequestStepScan({
return true;
})
.catch(() => {
- setUploadReceiptError(true, 'attachmentPicker.attachmentError', 'attachmentPicker.errorWhileSelectingCorruptedImage');
+ setUploadReceiptError(true, 'attachmentPicker.attachmentError', 'attachmentPicker.errorWhileSelectingCorruptedAttachment');
return false;
});
}
diff --git a/src/pages/iou/request/step/IOURequestStepWaypoint.tsx b/src/pages/iou/request/step/IOURequestStepWaypoint.tsx
index 6c0eae98fb85..1fbcebee6987 100644
--- a/src/pages/iou/request/step/IOURequestStepWaypoint.tsx
+++ b/src/pages/iou/request/step/IOURequestStepWaypoint.tsx
@@ -185,6 +185,7 @@ function IOURequestStepWaypoint({
prompt={translate('distance.deleteWaypointConfirmation')}
confirmText={translate('common.delete')}
cancelText={translate('common.cancel')}
+ shouldEnableNewFocusManagement
danger
/>
{
+ PaymentMethods.clearDebitCardFormErrorAndSubmit();
+
+ return () => {
+ PaymentMethods.clearDebitCardFormErrorAndSubmit();
+ };
+ }, []);
+
+ // TODO refactor ONYXKEYS.FORMS.ADD_DEBIT_CARD_FORM to ONYXKEYS.FORMS.ADD_CARD_FORM as a follow up
+ const addPaymentCard = useCallback((values: FormOnyxValues, currency?: ValueOf) => {
+ const cardData = {
+ cardNumber: values.cardNumber,
+ cardMonth: CardUtils.getMonthFromExpirationDateString(values.expirationDate),
+ cardYear: CardUtils.getYearFromExpirationDateString(values.expirationDate),
+ cardCVV: values.securityCode,
+ addressName: values.nameOnCard,
+ addressZip: values.addressZipCode,
+ currency: currency ?? CONST.CURRENCY.USD,
+ };
+ if (currency === CONST.CURRENCY.GBP) {
+ // TODO add AddPaymentCardGBP flow as a follow up
+ return;
+ }
+ PaymentMethods.addSubscriptionPaymentCard(cardData);
+ Navigation.goBack();
+ }, []);
+
+ return (
+
+
+
+ {translate('subscription.paymentCard.enterPaymentCardDetails')}}
+ footerContent={
+ <>
+ (
+
+ {translate('subscription.paymentCard.security')}{' '}
+
+ {translate('subscription.paymentCard.learnMoreAboutSecurity')}
+
+
+ )}
+ />
+ {/** TODO reusable component will be taken from https://github.com/Expensify/App/pull/42690 */}
+
+ From $5/active member with the Expensify Card, $10/active member without the Expensify Card.
+
+ >
+ }
+ />
+
+
+ );
+}
+
+AddPaymentCard.displayName = 'AddPaymentCard';
+
+export default AddPaymentCard;
diff --git a/src/pages/settings/Wallet/AddDebitCardPage.tsx b/src/pages/settings/Wallet/AddDebitCardPage.tsx
index 0beb3c16018d..0befaa55da52 100644
--- a/src/pages/settings/Wallet/AddDebitCardPage.tsx
+++ b/src/pages/settings/Wallet/AddDebitCardPage.tsx
@@ -1,67 +1,20 @@
-import {useRoute} from '@react-navigation/native';
import React, {useEffect, useRef} from 'react';
-import {View} from 'react-native';
-import type {OnyxEntry} from 'react-native-onyx';
-import {withOnyx} from 'react-native-onyx';
-import AddressSearch from '@components/AddressSearch';
-import CheckboxWithLabel from '@components/CheckboxWithLabel';
-import FormProvider from '@components/Form/FormProvider';
-import InputWrapper from '@components/Form/InputWrapper';
-import type {FormInputErrors, FormOnyxValues} from '@components/Form/types';
+import {useOnyx} from 'react-native-onyx';
+import PaymentCardForm from '@components/AddPaymentCard/PaymentCardForm';
import HeaderWithBackButton from '@components/HeaderWithBackButton';
import type {AnimatedTextInputRef} from '@components/RNTextInput';
import ScreenWrapper from '@components/ScreenWrapper';
-import StateSelector from '@components/StateSelector';
-import Text from '@components/Text';
-import TextInput from '@components/TextInput';
-import TextLink from '@components/TextLink';
import useLocalize from '@hooks/useLocalize';
import usePrevious from '@hooks/usePrevious';
-import useThemeStyles from '@hooks/useThemeStyles';
import Navigation from '@libs/Navigation/Navigation';
-import * as ValidationUtils from '@libs/ValidationUtils';
import * as PaymentMethods from '@userActions/PaymentMethods';
-import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
-import ROUTES from '@src/ROUTES';
-import SCREENS from '@src/SCREENS';
-import type {AddDebitCardForm} from '@src/types/form';
-import INPUT_IDS from '@src/types/form/AddDebitCardForm';
-type DebitCardPageOnyxProps = {
- /** Form data propTypes */
- formData: OnyxEntry;
-};
-
-type DebitCardPageProps = DebitCardPageOnyxProps;
-
-function IAcceptTheLabel() {
- const {translate} = useLocalize();
-
- return (
-
- {`${translate('common.iAcceptThe')}`}
- {`${translate('common.expensifyTermsOfService')}`}
-
- );
-}
-
-const REQUIRED_FIELDS = [
- INPUT_IDS.NAME_ON_CARD,
- INPUT_IDS.CARD_NUMBER,
- INPUT_IDS.EXPIRATION_DATE,
- INPUT_IDS.SECURITY_CODE,
- INPUT_IDS.ADDRESS_STREET,
- INPUT_IDS.ADDRESS_ZIP_CODE,
- INPUT_IDS.ADDRESS_STATE,
-];
-
-function DebitCardPage({formData}: DebitCardPageProps) {
- const styles = useThemeStyles();
+function DebitCardPage() {
const {translate} = useLocalize();
+ const [formData] = useOnyx(ONYXKEYS.FORMS.ADD_DEBIT_CARD_FORM);
const prevFormDataSetupComplete = usePrevious(!!formData?.setupComplete);
const nameOnCardRef = useRef(null);
- const route = useRoute();
/**
* Reset the form values on the mount and unmount so that old errors don't show when this form is displayed again.
@@ -82,43 +35,6 @@ function DebitCardPage({formData}: DebitCardPageProps) {
PaymentMethods.continueSetup();
}, [prevFormDataSetupComplete, formData?.setupComplete]);
- /**
- * @param values - form input values passed by the Form component
- */
- const validate = (values: FormOnyxValues): FormInputErrors => {
- const errors = ValidationUtils.getFieldRequiredErrors(values, REQUIRED_FIELDS);
-
- if (values.nameOnCard && !ValidationUtils.isValidLegalName(values.nameOnCard)) {
- errors.nameOnCard = 'addDebitCardPage.error.invalidName';
- }
-
- if (values.cardNumber && !ValidationUtils.isValidDebitCard(values.cardNumber.replace(/ /g, ''))) {
- errors.cardNumber = 'addDebitCardPage.error.debitCardNumber';
- }
-
- if (values.expirationDate && !ValidationUtils.isValidExpirationDate(values.expirationDate)) {
- errors.expirationDate = 'addDebitCardPage.error.expirationDate';
- }
-
- if (values.securityCode && !ValidationUtils.isValidSecurityCode(values.securityCode)) {
- errors.securityCode = 'addDebitCardPage.error.securityCode';
- }
-
- if (values.addressStreet && !ValidationUtils.isValidAddress(values.addressStreet)) {
- errors.addressStreet = 'addDebitCardPage.error.addressStreet';
- }
-
- if (values.addressZipCode && !ValidationUtils.isValidZipCode(values.addressZipCode)) {
- errors.addressZipCode = 'addDebitCardPage.error.addressZipCode';
- }
-
- if (!values.acceptTerms) {
- errors.acceptTerms = 'common.error.acceptTerms';
- }
-
- return errors;
- };
-
return (
nameOnCardRef.current?.focus()}
@@ -129,103 +45,19 @@ function DebitCardPage({formData}: DebitCardPageProps) {
title={translate('addDebitCardPage.addADebitCard')}
onBackButtonPress={() => Navigation.goBack()}
/>
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+ />
);
}
DebitCardPage.displayName = 'DebitCardPage';
-export default withOnyx({
- formData: {
- key: ONYXKEYS.FORMS.ADD_DEBIT_CARD_FORM,
- },
-})(DebitCardPage);
+export default DebitCardPage;
diff --git a/src/pages/signin/EmailDeliveryFailurePage.tsx b/src/pages/signin/EmailDeliveryFailurePage.tsx
index e2e12dbd0c1c..0a294e3a6b86 100644
--- a/src/pages/signin/EmailDeliveryFailurePage.tsx
+++ b/src/pages/signin/EmailDeliveryFailurePage.tsx
@@ -1,4 +1,4 @@
-import Str from 'expensify-common/lib/str';
+import {Str} from 'expensify-common';
import React, {useEffect, useMemo} from 'react';
import {Keyboard, View} from 'react-native';
import {withOnyx} from 'react-native-onyx';
diff --git a/src/pages/signin/LoginForm/BaseLoginForm.tsx b/src/pages/signin/LoginForm/BaseLoginForm.tsx
index 973eaa2d8907..aacaa0851cf7 100644
--- a/src/pages/signin/LoginForm/BaseLoginForm.tsx
+++ b/src/pages/signin/LoginForm/BaseLoginForm.tsx
@@ -1,5 +1,5 @@
import {useIsFocused} from '@react-navigation/native';
-import Str from 'expensify-common/lib/str';
+import {Str} from 'expensify-common';
import type {ForwardedRef} from 'react';
import React, {forwardRef, useCallback, useEffect, useImperativeHandle, useMemo, useRef, useState} from 'react';
import {View} from 'react-native';
diff --git a/src/pages/signin/SignInPage.tsx b/src/pages/signin/SignInPage.tsx
index 112d350087b1..437ac8724c96 100644
--- a/src/pages/signin/SignInPage.tsx
+++ b/src/pages/signin/SignInPage.tsx
@@ -1,4 +1,4 @@
-import Str from 'expensify-common/lib/str';
+import {Str} from 'expensify-common';
import React, {useEffect, useRef, useState} from 'react';
import {withOnyx} from 'react-native-onyx';
import type {OnyxEntry} from 'react-native-onyx';
diff --git a/src/pages/signin/UnlinkLoginForm.tsx b/src/pages/signin/UnlinkLoginForm.tsx
index c2e62e3b80f3..2fa43fbe61b2 100644
--- a/src/pages/signin/UnlinkLoginForm.tsx
+++ b/src/pages/signin/UnlinkLoginForm.tsx
@@ -1,4 +1,4 @@
-import Str from 'expensify-common/lib/str';
+import {Str} from 'expensify-common';
import React, {useMemo} from 'react';
import {View} from 'react-native';
import {withOnyx} from 'react-native-onyx';
diff --git a/src/pages/tasks/NewTaskDescriptionPage.tsx b/src/pages/tasks/NewTaskDescriptionPage.tsx
index 93ad5e63f3d7..410dd9c6e137 100644
--- a/src/pages/tasks/NewTaskDescriptionPage.tsx
+++ b/src/pages/tasks/NewTaskDescriptionPage.tsx
@@ -1,5 +1,5 @@
import type {StackScreenProps} from '@react-navigation/stack';
-import ExpensiMark from 'expensify-common/lib/ExpensiMark';
+import {ExpensiMark} from 'expensify-common';
import React from 'react';
import {View} from 'react-native';
import {withOnyx} from 'react-native-onyx';
diff --git a/src/pages/tasks/NewTaskDetailsPage.tsx b/src/pages/tasks/NewTaskDetailsPage.tsx
index 52fdbf5523a2..7cad3d5f844b 100644
--- a/src/pages/tasks/NewTaskDetailsPage.tsx
+++ b/src/pages/tasks/NewTaskDetailsPage.tsx
@@ -1,5 +1,5 @@
import type {StackScreenProps} from '@react-navigation/stack';
-import ExpensiMark from 'expensify-common/lib/ExpensiMark';
+import {ExpensiMark} from 'expensify-common';
import React, {useEffect, useState} from 'react';
import {View} from 'react-native';
import {withOnyx} from 'react-native-onyx';
diff --git a/src/pages/tasks/TaskDescriptionPage.tsx b/src/pages/tasks/TaskDescriptionPage.tsx
index c48746c81239..35e954bad2f1 100644
--- a/src/pages/tasks/TaskDescriptionPage.tsx
+++ b/src/pages/tasks/TaskDescriptionPage.tsx
@@ -1,5 +1,5 @@
import {useFocusEffect} from '@react-navigation/native';
-import ExpensiMark from 'expensify-common/lib/ExpensiMark';
+import {ExpensiMark} from 'expensify-common';
import React, {useCallback, useRef} from 'react';
import {View} from 'react-native';
import FullPageNotFoundView from '@components/BlockingViews/FullPageNotFoundView';
diff --git a/src/pages/wallet/WalletStatementPage.tsx b/src/pages/wallet/WalletStatementPage.tsx
index ac1b6d428d52..40f69d809618 100644
--- a/src/pages/wallet/WalletStatementPage.tsx
+++ b/src/pages/wallet/WalletStatementPage.tsx
@@ -1,6 +1,6 @@
import type {StackScreenProps} from '@react-navigation/stack';
import {format, getMonth, getYear} from 'date-fns';
-import Str from 'expensify-common/lib/str';
+import {Str} from 'expensify-common';
import React, {useEffect} from 'react';
import {withOnyx} from 'react-native-onyx';
import type {OnyxEntry} from 'react-native-onyx';
diff --git a/src/pages/workspace/WorkspaceInviteMessagePage.tsx b/src/pages/workspace/WorkspaceInviteMessagePage.tsx
index ffc7299eb1f0..028d6ef8a1d2 100644
--- a/src/pages/workspace/WorkspaceInviteMessagePage.tsx
+++ b/src/pages/workspace/WorkspaceInviteMessagePage.tsx
@@ -1,5 +1,5 @@
import type {StackScreenProps} from '@react-navigation/stack';
-import ExpensiMark from 'expensify-common/lib/ExpensiMark';
+import {ExpensiMark} from 'expensify-common';
import lodashDebounce from 'lodash/debounce';
import React, {useEffect, useMemo, useState} from 'react';
import {Keyboard, View} from 'react-native';
diff --git a/src/pages/workspace/WorkspaceMoreFeaturesPage.tsx b/src/pages/workspace/WorkspaceMoreFeaturesPage.tsx
index 548f3b21d077..05f72ac3c803 100644
--- a/src/pages/workspace/WorkspaceMoreFeaturesPage.tsx
+++ b/src/pages/workspace/WorkspaceMoreFeaturesPage.tsx
@@ -185,6 +185,7 @@ function WorkspaceMoreFeaturesPage({policy, route}: WorkspaceMoreFeaturesPagePro
(null);
- const isSyncInProgress = !!connectionSyncProgress?.stageInProgress && connectionSyncProgress.stageInProgress !== CONST.POLICY.CONNECTIONS.SYNC_STAGE_NAME.JOB_DONE;
+ const lastSyncProgressDate = parseISO(connectionSyncProgress?.timestamp ?? '');
+ const isSyncInProgress =
+ !!connectionSyncProgress?.stageInProgress &&
+ connectionSyncProgress.stageInProgress !== CONST.POLICY.CONNECTIONS.SYNC_STAGE_NAME.JOB_DONE &&
+ isValid(lastSyncProgressDate) &&
+ differenceInMinutes(new Date(), lastSyncProgressDate) < CONST.POLICY.CONNECTIONS.SYNC_STAGE_TIMEOUT_MINUTES;
const accountingIntegrations = Object.values(CONST.POLICY.CONNECTIONS.NAME).filter((name) => !(name === CONST.POLICY.CONNECTIONS.NAME.XERO && !canUseXeroIntegration));
const connectedIntegration = accountingIntegrations.find((integration) => !!policy?.connections?.[integration]) ?? connectionSyncProgress?.connectionName;
diff --git a/src/pages/workspace/bills/WorkspaceBillsFirstSection.tsx b/src/pages/workspace/bills/WorkspaceBillsFirstSection.tsx
index 638ab9d58c31..f30d90f5a063 100644
--- a/src/pages/workspace/bills/WorkspaceBillsFirstSection.tsx
+++ b/src/pages/workspace/bills/WorkspaceBillsFirstSection.tsx
@@ -1,4 +1,4 @@
-import Str from 'expensify-common/lib/str';
+import {Str} from 'expensify-common';
import React from 'react';
import {View} from 'react-native';
import {withOnyx} from 'react-native-onyx';
diff --git a/src/pages/workspace/distanceRates/PolicyDistanceRateDetailsPage.tsx b/src/pages/workspace/distanceRates/PolicyDistanceRateDetailsPage.tsx
index 012ba712d0aa..7f604efd20ee 100644
--- a/src/pages/workspace/distanceRates/PolicyDistanceRateDetailsPage.tsx
+++ b/src/pages/workspace/distanceRates/PolicyDistanceRateDetailsPage.tsx
@@ -9,6 +9,7 @@ import * as Expensicons from '@components/Icon/Expensicons';
import MenuItemWithTopDescription from '@components/MenuItemWithTopDescription';
import OfflineWithFeedback from '@components/OfflineWithFeedback';
import ScreenWrapper from '@components/ScreenWrapper';
+import ScrollView from '@components/ScrollView';
import Switch from '@components/Switch';
import Text from '@components/Text';
import useLocalize from '@hooks/useLocalize';
@@ -41,14 +42,17 @@ function PolicyDistanceRateDetailsPage({policy, route}: PolicyDistanceRateDetail
const {windowWidth} = useWindowDimensions();
const [isWarningModalVisible, setIsWarningModalVisible] = useState(false);
const [isDeleteModalVisible, setIsDeleteModalVisible] = useState(false);
-
const policyID = route.params.policyID;
const rateID = route.params.rateID;
const customUnits = policy?.customUnits ?? {};
const customUnit = customUnits[Object.keys(customUnits)[0]];
const rate = customUnit?.rates[rateID];
const currency = rate?.currency ?? CONST.CURRENCY.USD;
+ const taxClaimablePercentage = rate.attributes?.taxClaimablePercentage;
+ const taxRateExternalID = rate.attributes?.taxRateExternalID;
+ const isDistanceTrackTaxEnabled = !!customUnit?.attributes?.taxEnabled;
+ const taxRate = taxRateExternalID ? `${policy?.taxRates?.taxes[taxRateExternalID].name} (${policy?.taxRates?.taxes[taxRateExternalID].value})` : '';
// Rates can be disabled or deleted as long as in the remaining rates there is always at least one enabled rate and there are no pending delete action
const canDisableOrDeleteRate = Object.values(customUnit?.rates ?? {}).some(
(distanceRate: Rate) => distanceRate?.enabled && rateID !== distanceRate?.customUnitRateID && distanceRate?.pendingAction !== CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE,
@@ -62,6 +66,12 @@ function PolicyDistanceRateDetailsPage({policy, route}: PolicyDistanceRateDetail
const editRateValue = () => {
Navigation.navigate(ROUTES.WORKSPACE_DISTANCE_RATE_EDIT.getRoute(policyID, rateID));
};
+ const editTaxReclaimableValue = () => {
+ Navigation.navigate(ROUTES.WORKSPACE_DISTANCE_RATE_TAX_RECLAIMABLE_ON_EDIT.getRoute(policyID, rateID));
+ };
+ const editTaxRateValue = () => {
+ Navigation.navigate(ROUTES.WORKSPACE_DISTANCE_RATE_TAX_RATE_EDIT.getRoute(policyID, rateID));
+ };
const toggleRate = () => {
if (!rate?.enabled || canDisableOrDeleteRate) {
@@ -78,6 +88,7 @@ function PolicyDistanceRateDetailsPage({policy, route}: PolicyDistanceRateDetail
};
const rateValueToDisplay = CurrencyUtils.convertAmountToDisplayString(rate?.rate, currency);
+ const taxClaimableValueToDisplay = taxClaimablePercentage && rate.rate ? CurrencyUtils.convertAmountToDisplayString(taxClaimablePercentage * rate.rate, currency) : '';
const unitToDisplay = translate(`common.${customUnit?.attributes?.unit ?? CONST.CUSTOM_UNITS.DISTANCE_UNIT_MILES}`);
const threeDotsMenuItems = [
@@ -115,7 +126,7 @@ function PolicyDistanceRateDetailsPage({policy, route}: PolicyDistanceRateDetail
threeDotsMenuItems={threeDotsMenuItems}
threeDotsAnchorPosition={styles.threeDotsPopoverOffset(windowWidth)}
/>
-
+
+ {isDistanceTrackTaxEnabled && (
+ clearErrorFields('attributes')}
+ >
+
+
+
+
+ )}
+ {isDistanceTrackTaxEnabled && (
+ clearErrorFields('attributes')}
+ >
+
+
+ )}
setIsWarningModalVisible(false)}
isVisible={isWarningModalVisible}
@@ -163,7 +207,7 @@ function PolicyDistanceRateDetailsPage({policy, route}: PolicyDistanceRateDetail
cancelText={translate('common.cancel')}
danger
/>
-
+
);
diff --git a/src/pages/workspace/distanceRates/PolicyDistanceRateTaxRateEditPage.tsx b/src/pages/workspace/distanceRates/PolicyDistanceRateTaxRateEditPage.tsx
new file mode 100644
index 000000000000..e8b58b70f474
--- /dev/null
+++ b/src/pages/workspace/distanceRates/PolicyDistanceRateTaxRateEditPage.tsx
@@ -0,0 +1,90 @@
+import type {StackScreenProps} from '@react-navigation/stack';
+import React, {useMemo} from 'react';
+import HeaderWithBackButton from '@components/HeaderWithBackButton';
+import ScreenWrapper from '@components/ScreenWrapper';
+import SelectionList from '@components/SelectionList';
+import RadioListItem from '@components/SelectionList/RadioListItem';
+import useLocalize from '@hooks/useLocalize';
+import useThemeStyles from '@hooks/useThemeStyles';
+import Navigation from '@libs/Navigation/Navigation';
+import type {SettingsNavigatorParamList} from '@navigation/types';
+import AccessOrNotFoundWrapper from '@pages/workspace/AccessOrNotFoundWrapper';
+import type {WithPolicyOnyxProps} from '@pages/workspace/withPolicy';
+import withPolicy from '@pages/workspace/withPolicy';
+import * as DistanceRate from '@userActions/Policy/DistanceRate';
+import CONST from '@src/CONST';
+import ROUTES from '@src/ROUTES';
+import type SCREENS from '@src/SCREENS';
+
+type ListItemType = {
+ value: string;
+ text: string;
+ isSelected: boolean;
+ keyForList: string;
+};
+
+type PolicyDistanceRateTaxRateEditPageProps = WithPolicyOnyxProps & StackScreenProps;
+
+function PolicyDistanceRateTaxRateEditPage({route, policy}: PolicyDistanceRateTaxRateEditPageProps) {
+ const styles = useThemeStyles();
+ const {translate} = useLocalize();
+ const policyID = route.params.policyID;
+ const rateID = route.params.rateID;
+ const customUnits = policy?.customUnits ?? {};
+ const customUnit = customUnits[Object.keys(customUnits)[0]];
+ const rate = customUnit?.rates[rateID];
+ const taxRateExternalID = rate.attributes?.taxRateExternalID;
+ const taxRateItems: ListItemType[] = useMemo(() => {
+ const taxes = policy?.taxRates?.taxes;
+ const result = Object.entries(taxes ?? {}).map(([key, value]) => ({
+ value: key,
+ text: `${value.name} (${value.value})`,
+ isSelected: taxRateExternalID === key,
+ keyForList: key,
+ }));
+ return result;
+ }, [policy, taxRateExternalID]);
+
+ const onTaxRateChange = (newTaxRate: ListItemType) => {
+ DistanceRate.updateDistanceTaxRate(policyID, customUnit, [
+ {
+ ...rate,
+ attributes: {
+ ...rate.attributes,
+ taxRateExternalID: newTaxRate.value,
+ },
+ },
+ ]);
+ Navigation.navigate(ROUTES.WORKSPACE_DISTANCE_RATE_DETAILS.getRoute(policyID, rateID));
+ };
+
+ return (
+
+
+
+ item.isSelected)?.keyForList}
+ />
+
+
+ );
+}
+
+PolicyDistanceRateTaxRateEditPage.displayName = 'PolicyDistanceRateTaxRateEditPage';
+
+export default withPolicy(PolicyDistanceRateTaxRateEditPage);
diff --git a/src/pages/workspace/distanceRates/PolicyDistanceRateTaxReclaimableEditPage.tsx b/src/pages/workspace/distanceRates/PolicyDistanceRateTaxReclaimableEditPage.tsx
new file mode 100644
index 000000000000..7be33360de27
--- /dev/null
+++ b/src/pages/workspace/distanceRates/PolicyDistanceRateTaxReclaimableEditPage.tsx
@@ -0,0 +1,102 @@
+import type {StackScreenProps} from '@react-navigation/stack';
+import React, {useCallback} from 'react';
+import AmountForm from '@components/AmountForm';
+import FormProvider from '@components/Form/FormProvider';
+import InputWrapperWithRef from '@components/Form/InputWrapper';
+import type {FormOnyxValues} from '@components/Form/types';
+import HeaderWithBackButton from '@components/HeaderWithBackButton';
+import ScreenWrapper from '@components/ScreenWrapper';
+import useAutoFocusInput from '@hooks/useAutoFocusInput';
+import useLocalize from '@hooks/useLocalize';
+import useThemeStyles from '@hooks/useThemeStyles';
+import * as CurrencyUtils from '@libs/CurrencyUtils';
+import Navigation from '@libs/Navigation/Navigation';
+import {validateTaxClaimableValue} from '@libs/PolicyDistanceRatesUtils';
+import type {SettingsNavigatorParamList} from '@navigation/types';
+import AccessOrNotFoundWrapper from '@pages/workspace/AccessOrNotFoundWrapper';
+import type {WithPolicyOnyxProps} from '@pages/workspace/withPolicy';
+import withPolicy from '@pages/workspace/withPolicy';
+import * as DistanceRate from '@userActions/Policy/DistanceRate';
+import CONST from '@src/CONST';
+import ONYXKEYS from '@src/ONYXKEYS';
+import type SCREENS from '@src/SCREENS';
+import INPUT_IDS from '@src/types/form/PolicyDistanceRateTaxReclaimableOnEditForm';
+
+type PolicyDistanceRateTaxReclaimableEditPageProps = WithPolicyOnyxProps & StackScreenProps;
+
+function PolicyDistanceRateTaxReclaimableEditPage({route, policy}: PolicyDistanceRateTaxReclaimableEditPageProps) {
+ const styles = useThemeStyles();
+ const {translate} = useLocalize();
+ const {inputCallbackRef} = useAutoFocusInput();
+
+ const policyID = route.params.policyID;
+ const rateID = route.params.rateID;
+ const customUnits = policy?.customUnits ?? {};
+ const customUnit = customUnits[Object.keys(customUnits)[0]];
+ const rate = customUnit.rates[rateID];
+ const currency = rate.currency ?? CONST.CURRENCY.USD;
+ const extraDecimals = 1;
+ const decimals = CurrencyUtils.getCurrencyDecimals(currency) + extraDecimals;
+ const currentTaxReclaimableOnValue = rate.attributes?.taxClaimablePercentage && rate.rate ? ((rate.attributes.taxClaimablePercentage * rate.rate) / 100).toFixed(decimals) : '';
+
+ const submitTaxReclaimableOn = (values: FormOnyxValues) => {
+ DistanceRate.updateDistanceTaxClaimableValue(policyID, customUnit, [
+ {
+ ...rate,
+ attributes: {
+ ...rate.attributes,
+ taxClaimablePercentage: rate.rate ? (Number(values.taxClaimableValue) * 100) / rate.rate : undefined,
+ },
+ },
+ ]);
+ Navigation.goBack();
+ };
+
+ const validate = useCallback((values: FormOnyxValues) => validateTaxClaimableValue(values, rate), [rate]);
+
+ return (
+
+
+
+
+
+
+
+
+ );
+}
+
+PolicyDistanceRateTaxReclaimableEditPage.displayName = 'PolicyDistanceRateTaxReclaimableEditPage';
+
+export default withPolicy(PolicyDistanceRateTaxReclaimableEditPage);
diff --git a/src/pages/workspace/distanceRates/PolicyDistanceRatesSettingsPage.tsx b/src/pages/workspace/distanceRates/PolicyDistanceRatesSettingsPage.tsx
index 15f3aabd76e0..2f1b9317e0a9 100644
--- a/src/pages/workspace/distanceRates/PolicyDistanceRatesSettingsPage.tsx
+++ b/src/pages/workspace/distanceRates/PolicyDistanceRatesSettingsPage.tsx
@@ -6,18 +6,25 @@ import {withOnyx} from 'react-native-onyx';
import HeaderWithBackButton from '@components/HeaderWithBackButton';
import OfflineWithFeedback from '@components/OfflineWithFeedback';
import ScreenWrapper from '@components/ScreenWrapper';
+import ScrollView from '@components/ScrollView';
import type {ListItem} from '@components/SelectionList/types';
+import Switch from '@components/Switch';
+import Text from '@components/Text';
+import TextLink from '@components/TextLink';
import type {UnitItemType} from '@components/UnitPicker';
import useLocalize from '@hooks/useLocalize';
import useThemeStyles from '@hooks/useThemeStyles';
import * as ErrorUtils from '@libs/ErrorUtils';
+import Navigation from '@libs/Navigation/Navigation';
import * as OptionsListUtils from '@libs/OptionsListUtils';
import type {SettingsNavigatorParamList} from '@navigation/types';
import AccessOrNotFoundWrapper from '@pages/workspace/AccessOrNotFoundWrapper';
import * as Category from '@userActions/Policy/Category';
import * as DistanceRate from '@userActions/Policy/DistanceRate';
+import * as Policy from '@userActions/Policy/Policy';
import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
+import ROUTES from '@src/ROUTES';
import type SCREENS from '@src/SCREENS';
import type * as OnyxTypes from '@src/types/onyx';
import type {CustomUnit} from '@src/types/onyx/Policy';
@@ -37,11 +44,12 @@ type PolicyDistanceRatesSettingsPageProps = PolicyDistanceRatesSettingsPageOnyxP
function PolicyDistanceRatesSettingsPage({policy, policyCategories, route}: PolicyDistanceRatesSettingsPageProps) {
const styles = useThemeStyles();
const {translate} = useLocalize();
-
const policyID = route.params.policyID;
const customUnits = policy?.customUnits ?? {};
const customUnit = customUnits[Object.keys(customUnits)[0]];
const customUnitID = customUnit?.customUnitID ?? '';
+ const isDistanceTrackTaxEnabled = !!customUnit?.attributes?.taxEnabled;
+ const isPolicyTrackTaxEnabled = !!policy?.tax?.trackingEnabled;
const defaultCategory = customUnits[customUnitID]?.defaultCategory;
const defaultUnit = customUnits[customUnitID]?.attributes.unit;
@@ -66,6 +74,10 @@ function PolicyDistanceRatesSettingsPage({policy, policyCategories, route}: Poli
DistanceRate.clearPolicyDistanceRatesErrorFields(policyID, customUnitID, {...errorFields, [fieldName]: null});
};
+ const onToggleTrackTax = (isOn: boolean) => {
+ const attributes = {...customUnits[customUnitID].attributes, taxEnabled: isOn};
+ Policy.enableDistanceRequestTax(policyID, customUnit.name, customUnitID, attributes);
+ };
return (
-
- clearErrorFields('attributes')}
- >
-
-
- {policy?.areCategoriesEnabled && OptionsListUtils.hasEnabledOptions(policyCategories ?? {}) && (
+
+
clearErrorFields('defaultCategory')}
+ onClose={() => clearErrorFields('attributes')}
>
-
- )}
-
+ {policy?.areCategoriesEnabled && OptionsListUtils.hasEnabledOptions(policyCategories ?? {}) && (
+ clearErrorFields('defaultCategory')}
+ >
+
+
+ )}
+
+
+
+ {translate('workspace.distanceRates.trackTax')}
+
+
+
+ {!isPolicyTrackTaxEnabled && (
+
+
+ {translate('workspace.distanceRates.taxFeatureNotEnabledMessage')}
+ {
+ Navigation.dismissModal();
+ Navigation.navigate(ROUTES.WORKSPACE_MORE_FEATURES.getRoute(policyID));
+ }}
+ >
+ {translate('workspace.common.moreFeatures')}
+
+ {translate('workspace.distanceRates.changePromptMessage')}
+
+
+ )}
+
+
+
);
diff --git a/src/pages/workspace/distanceRates/UnitSelector/index.tsx b/src/pages/workspace/distanceRates/UnitSelector/index.tsx
index e6511e7d6418..166d0df786a1 100644
--- a/src/pages/workspace/distanceRates/UnitSelector/index.tsx
+++ b/src/pages/workspace/distanceRates/UnitSelector/index.tsx
@@ -1,4 +1,4 @@
-import Str from 'expensify-common/lib/str';
+import {Str} from 'expensify-common';
import React, {useState} from 'react';
import type {StyleProp, ViewStyle} from 'react-native';
import {View} from 'react-native';
diff --git a/src/pages/workspace/members/WorkspaceOwnerChangeWrapperPage.tsx b/src/pages/workspace/members/WorkspaceOwnerChangeWrapperPage.tsx
index b32c04a5c4aa..bf5e8cb869e2 100644
--- a/src/pages/workspace/members/WorkspaceOwnerChangeWrapperPage.tsx
+++ b/src/pages/workspace/members/WorkspaceOwnerChangeWrapperPage.tsx
@@ -67,7 +67,7 @@ function WorkspaceOwnerChangeWrapperPage({route, policy}: WorkspaceOwnerChangeWr
Navigation.navigate(ROUTES.WORKSPACE_MEMBER_DETAILS.getRoute(policyID, accountID));
}}
/>
-
+
{policy?.isLoading && }
{!policy?.isLoading &&
(error === CONST.POLICY.OWNERSHIP_ERRORS.NO_BILLING_CARD ? (
diff --git a/src/pages/workspace/members/WorkspaceOwnerPaymentCardForm.tsx b/src/pages/workspace/members/WorkspaceOwnerPaymentCardForm.tsx
index 1a2f32449c41..31e40473d33f 100644
--- a/src/pages/workspace/members/WorkspaceOwnerPaymentCardForm.tsx
+++ b/src/pages/workspace/members/WorkspaceOwnerPaymentCardForm.tsx
@@ -1,47 +1,33 @@
-import React, {useCallback, useEffect, useRef, useState} from 'react';
+import React, {useCallback, useEffect, useState} from 'react';
import {View} from 'react-native';
import type {OnyxEntry} from 'react-native-onyx';
-import FormProvider from '@components/Form/FormProvider';
-import InputWrapper from '@components/Form/InputWrapper';
-import type {FormInputErrors, FormOnyxValues} from '@components/Form/types';
-import Hoverable from '@components/Hoverable';
+import PaymentCardForm from '@components/AddPaymentCard/PaymentCardForm';
+import type {FormOnyxValues} from '@components/Form/types';
import Icon from '@components/Icon';
import * as Expensicons from '@components/Icon/Expensicons';
import * as Illustrations from '@components/Icon/Illustrations';
-import type {AnimatedTextInputRef} from '@components/RNTextInput';
import Section, {CARD_LAYOUT} from '@components/Section';
import Text from '@components/Text';
-import TextInput from '@components/TextInput';
import TextLink from '@components/TextLink';
import useLocalize from '@hooks/useLocalize';
import useTheme from '@hooks/useTheme';
import useThemeStyles from '@hooks/useThemeStyles';
import * as CardUtils from '@libs/CardUtils';
-import * as ValidationUtils from '@libs/ValidationUtils';
import * as PaymentMethods from '@userActions/PaymentMethods';
import * as PolicyActions from '@userActions/Policy/Policy';
import CONST from '@src/CONST';
-import ONYXKEYS from '@src/ONYXKEYS';
-import INPUT_IDS from '@src/types/form/AddDebitCardForm';
+import type ONYXKEYS from '@src/ONYXKEYS';
import type * as OnyxTypes from '@src/types/onyx';
-import WorkspaceOwnerPaymentCardCurrencyModal from './WorkspaceOwnerPaymentCardCurrencyModal';
type WorkspaceOwnerPaymentCardFormProps = {
/** The policy */
policy: OnyxEntry;
};
-const REQUIRED_FIELDS = [INPUT_IDS.NAME_ON_CARD, INPUT_IDS.CARD_NUMBER, INPUT_IDS.EXPIRATION_DATE, INPUT_IDS.ADDRESS_STREET, INPUT_IDS.SECURITY_CODE, INPUT_IDS.ADDRESS_ZIP_CODE];
-
function WorkspaceOwnerPaymentCardForm({policy}: WorkspaceOwnerPaymentCardFormProps) {
- const styles = useThemeStyles();
- const theme = useTheme();
const {translate} = useLocalize();
-
- const cardNumberRef = useRef(null);
-
- const [isCurrencyModalVisible, setIsCurrencyModalVisible] = useState(false);
- const [currency, setCurrency] = useState(CONST.CURRENCY.USD);
+ const theme = useTheme();
+ const styles = useThemeStyles();
const [shouldShowPaymentCardForm, setShouldShowPaymentCardForm] = useState(false);
const policyID = policy?.id ?? '';
@@ -72,36 +58,6 @@ function WorkspaceOwnerPaymentCardForm({policy}: WorkspaceOwnerPaymentCardFormPr
checkIfCanBeRendered();
}, [checkIfCanBeRendered]);
- const validate = (formValues: FormOnyxValues): FormInputErrors => {
- const errors = ValidationUtils.getFieldRequiredErrors(formValues, REQUIRED_FIELDS);
-
- if (formValues.nameOnCard && !ValidationUtils.isValidLegalName(formValues.nameOnCard)) {
- errors.nameOnCard = 'addDebitCardPage.error.invalidName';
- }
-
- if (formValues.cardNumber && !ValidationUtils.isValidDebitCard(formValues.cardNumber.replace(/ /g, ''))) {
- errors.cardNumber = 'addDebitCardPage.error.debitCardNumber';
- }
-
- if (formValues.expirationDate && !ValidationUtils.isValidExpirationDate(formValues.expirationDate)) {
- errors.expirationDate = 'addDebitCardPage.error.expirationDate';
- }
-
- if (formValues.securityCode && !ValidationUtils.isValidSecurityCode(formValues.securityCode)) {
- errors.securityCode = 'addDebitCardPage.error.securityCode';
- }
-
- if (formValues.addressStreet && !ValidationUtils.isValidAddress(formValues.addressStreet)) {
- errors.addressStreet = 'addDebitCardPage.error.addressStreet';
- }
-
- if (formValues.addressZipCode && !ValidationUtils.isValidZipCode(formValues.addressZipCode)) {
- errors.addressZipCode = 'addDebitCardPage.error.addressZipCode';
- }
-
- return errors;
- };
-
const addPaymentCard = useCallback(
(values: FormOnyxValues) => {
const cardData = {
@@ -119,174 +75,78 @@ function WorkspaceOwnerPaymentCardForm({policy}: WorkspaceOwnerPaymentCardFormPr
[policyID],
);
- const showCurrenciesModal = useCallback(() => {
- setIsCurrencyModalVisible(true);
- }, []);
-
- const changeCurrency = useCallback((newCurrency: keyof typeof CONST.CURRENCY) => {
- setCurrency(newCurrency);
- setIsCurrencyModalVisible(false);
- }, []);
-
- if (!shouldShowPaymentCardForm) {
- return null;
- }
-
return (
- <>
- {translate('workspace.changeOwner.addPaymentCardTitle')}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- {(isHovered) => (
-
- )}
-
-
-
-
- }
- currentCurrency={currency}
- onCurrencyChange={changeCurrency}
- onClose={() => setIsCurrencyModalVisible(false)}
- />
-
-
- {translate('workspace.changeOwner.addPaymentCardReadAndAcceptTextPart1')}{' '}
-
- {translate('workspace.changeOwner.addPaymentCardTerms')}
- {' '}
- {translate('workspace.changeOwner.addPaymentCardAnd')}{' '}
-
- {translate('workspace.changeOwner.addPaymentCardPrivacy')}
- {' '}
- {translate('workspace.changeOwner.addPaymentCardReadAndAcceptTextPart2')}
-
-
-
-
-
- {translate('workspace.changeOwner.addPaymentCardPciCompliant')}
-
-
-
- {translate('workspace.changeOwner.addPaymentCardBankLevelEncrypt')}
-
-
-
- {translate('workspace.changeOwner.addPaymentCardRedundant')}
-
-
-
- {translate('workspace.changeOwner.addPaymentCardLearnMore')}{' '}
+ {translate('workspace.changeOwner.addPaymentCardTitle')}}
+ footerContent={
+ <>
+
+ {translate('workspace.changeOwner.addPaymentCardReadAndAcceptTextPart1')}{' '}
- {translate('workspace.changeOwner.addPaymentCardSecurity')}
-
- .
+ {translate('workspace.changeOwner.addPaymentCardTerms')}
+ {' '}
+ {translate('workspace.changeOwner.addPaymentCardAnd')}{' '}
+
+ {translate('workspace.changeOwner.addPaymentCardPrivacy')}
+ {' '}
+ {translate('workspace.changeOwner.addPaymentCardReadAndAcceptTextPart2')}
-
-
- >
+
+
+
+
+ {translate('workspace.changeOwner.addPaymentCardPciCompliant')}
+
+
+
+ {translate('workspace.changeOwner.addPaymentCardBankLevelEncrypt')}
+
+
+
+ {translate('workspace.changeOwner.addPaymentCardRedundant')}
+
+
+
+ {translate('workspace.changeOwner.addPaymentCardLearnMore')}{' '}
+
+ {translate('workspace.changeOwner.addPaymentCardSecurity')}
+
+ .
+
+
+ >
+ }
+ />
);
}
diff --git a/src/pages/workspace/reimburse/WorkspaceRateAndUnitPage/InitialPage.tsx b/src/pages/workspace/reimburse/WorkspaceRateAndUnitPage/InitialPage.tsx
index 4e1821c6aad6..0a384728f00d 100644
--- a/src/pages/workspace/reimburse/WorkspaceRateAndUnitPage/InitialPage.tsx
+++ b/src/pages/workspace/reimburse/WorkspaceRateAndUnitPage/InitialPage.tsx
@@ -1,4 +1,4 @@
-import Str from 'expensify-common/lib/str';
+import {Str} from 'expensify-common';
import React, {useEffect} from 'react';
import {View} from 'react-native';
import {withOnyx} from 'react-native-onyx';
diff --git a/src/pages/workspace/taxes/NamePage.tsx b/src/pages/workspace/taxes/NamePage.tsx
index 53b8964787d4..6b37a86b3bd5 100644
--- a/src/pages/workspace/taxes/NamePage.tsx
+++ b/src/pages/workspace/taxes/NamePage.tsx
@@ -1,5 +1,5 @@
import type {StackScreenProps} from '@react-navigation/stack';
-import ExpensiMark from 'expensify-common/lib/ExpensiMark';
+import {ExpensiMark} from 'expensify-common';
import React, {useCallback, useState} from 'react';
import {View} from 'react-native';
import FormProvider from '@components/Form/FormProvider';
diff --git a/src/pages/workspace/taxes/WorkspaceCreateTaxPage.tsx b/src/pages/workspace/taxes/WorkspaceCreateTaxPage.tsx
index 1301ad100d77..d64d8f5dac39 100644
--- a/src/pages/workspace/taxes/WorkspaceCreateTaxPage.tsx
+++ b/src/pages/workspace/taxes/WorkspaceCreateTaxPage.tsx
@@ -2,6 +2,7 @@ import type {StackScreenProps} from '@react-navigation/stack';
import React, {useCallback} from 'react';
import {View} from 'react-native';
import AmountPicker from '@components/AmountPicker';
+import FullPageNotFoundView from '@components/BlockingViews/FullPageNotFoundView';
import FormProvider from '@components/Form/FormProvider';
import InputWrapper from '@components/Form/InputWrapper';
import type {FormInputErrors, FormOnyxValues} from '@components/Form/types';
@@ -14,6 +15,7 @@ import useThemeStyles from '@hooks/useThemeStyles';
import {createPolicyTax, getNextTaxCode, getTaxValueWithPercentage, validateTaxName, validateTaxValue} from '@libs/actions/TaxRate';
import Navigation from '@libs/Navigation/Navigation';
import type {SettingsNavigatorParamList} from '@libs/Navigation/types';
+import * as PolicyUtils from '@libs/PolicyUtils';
import AccessOrNotFoundWrapper from '@pages/workspace/AccessOrNotFoundWrapper';
import type {WithPolicyAndFullscreenLoadingProps} from '@pages/workspace/withPolicyAndFullscreenLoading';
import withPolicyAndFullscreenLoading from '@pages/workspace/withPolicyAndFullscreenLoading';
@@ -71,46 +73,48 @@ function WorkspaceCreateTaxPage({
includeSafeAreaPaddingBottom={false}
style={[styles.defaultModalContainer]}
>
-
-
-
-
-
- (v ? getTaxValueWithPercentage(v) : '')}
- description={translate('workspace.taxes.value')}
- rightLabel={translate('common.required')}
- hideCurrencySymbol
- // The default currency uses 2 decimal places, so we substract it
- extraDecimals={CONST.MAX_TAX_RATE_DECIMAL_PLACES - 2}
- // We increase the amount max length to support the extra decimals.
- amountMaxLength={CONST.MAX_TAX_RATE_DECIMAL_PLACES + CONST.MAX_TAX_RATE_INTEGER_PLACES}
- extraSymbol={%}
- />
-
-
-
+
+
+
+
+
+
+ (v ? getTaxValueWithPercentage(v) : '')}
+ description={translate('workspace.taxes.value')}
+ rightLabel={translate('common.required')}
+ hideCurrencySymbol
+ // The default currency uses 2 decimal places, so we substract it
+ extraDecimals={CONST.MAX_TAX_RATE_DECIMAL_PLACES - 2}
+ // We increase the amount max length to support the extra decimals.
+ amountMaxLength={CONST.MAX_TAX_RATE_DECIMAL_PLACES + CONST.MAX_TAX_RATE_INTEGER_PLACES}
+ extraSymbol={%}
+ />
+
+
+
+
);
diff --git a/src/pages/workspace/withPolicy.tsx b/src/pages/workspace/withPolicy.tsx
index 56ad756194a0..86d48facc386 100644
--- a/src/pages/workspace/withPolicy.tsx
+++ b/src/pages/workspace/withPolicy.tsx
@@ -39,6 +39,8 @@ type PolicyRoute = RouteProp<
| typeof SCREENS.WORKSPACE.OWNER_CHANGE_CHECK
| typeof SCREENS.WORKSPACE.TAX_EDIT
| typeof SCREENS.WORKSPACE.ADDRESS
+ | typeof SCREENS.WORKSPACE.DISTANCE_RATE_TAX_RATE_EDIT
+ | typeof SCREENS.WORKSPACE.DISTANCE_RATE_TAX_RECLAIMABLE_ON_EDIT
>;
function getPolicyIDFromRoute(route: PolicyRoute): string {
diff --git a/src/pages/workspace/workflows/ToggleSettingsOptionRow.tsx b/src/pages/workspace/workflows/ToggleSettingsOptionRow.tsx
index 913302fb2271..f40f721bf4c6 100644
--- a/src/pages/workspace/workflows/ToggleSettingsOptionRow.tsx
+++ b/src/pages/workspace/workflows/ToggleSettingsOptionRow.tsx
@@ -1,6 +1,6 @@
import React, {useMemo} from 'react';
import {View} from 'react-native';
-import type {StyleProp, ViewStyle} from 'react-native';
+import type {StyleProp, TextStyle, ViewStyle} from 'react-native';
import Icon from '@components/Icon';
import OfflineWithFeedback from '@components/OfflineWithFeedback';
import Switch from '@components/Switch';
@@ -22,6 +22,8 @@ type ToggleSettingOptionRowProps = {
shouldPlaceSubtitleBelowSwitch?: boolean;
/** Used to apply styles to the outermost container */
wrapperStyle?: StyleProp;
+ /** Used to apply styles to the Title */
+ titleStyle?: StyleProp;
/** Whether the option is enabled or not */
isActive: boolean;
/** Callback to be called when the switch is toggled */
@@ -49,6 +51,7 @@ function ToggleSettingOptionRow({
switchAccessibilityLabel,
shouldPlaceSubtitleBelowSwitch,
wrapperStyle,
+ titleStyle,
onToggle,
subMenuItems,
isActive,
@@ -85,7 +88,7 @@ function ToggleSettingOptionRow({
/>
)}
- {title}
+ {title}
{!shouldPlaceSubtitleBelowSwitch && subtitle && subTitleView}
diff --git a/src/pages/workspace/workflows/WorkspaceWorkflowsPage.tsx b/src/pages/workspace/workflows/WorkspaceWorkflowsPage.tsx
index 5b17a4e26051..aa779e6b9ea1 100644
--- a/src/pages/workspace/workflows/WorkspaceWorkflowsPage.tsx
+++ b/src/pages/workspace/workflows/WorkspaceWorkflowsPage.tsx
@@ -261,6 +261,7 @@ function WorkspaceWorkflowsPage({policy, betas, route}: WorkspaceWorkflowsPagePr
cardSectionContainer: {
backgroundColor: theme.cardBG,
- borderRadius: variables.componentBorderRadiusCard,
+ borderRadius: variables.componentBorderRadiusLarge,
width: 'auto',
textAlign: 'left',
overflow: 'hidden',
@@ -4419,6 +4419,16 @@ const styles = (theme: ThemeColors) =>
maxWidth: 400,
},
+ pdfErrorPlaceholder: {
+ overflow: 'hidden',
+ borderWidth: 2,
+ borderColor: theme.cardBG,
+ borderRadius: variables.componentBorderRadiusLarge,
+ maxWidth: 400,
+ height: '100%',
+ backgroundColor: theme.highlightBG,
+ },
+
moneyRequestAttachReceipt: {
backgroundColor: theme.highlightBG,
borderColor: theme.border,
@@ -4958,6 +4968,11 @@ const styles = (theme: ThemeColors) =>
textDecorationLine: 'line-through',
},
+ tripIllustrationSize: {
+ width: 190,
+ height: 172,
+ },
+
reportListItemSeparator: {
borderBottomWidth: 1,
borderBottomColor: theme.activeComponentBG,
diff --git a/src/styles/utils/index.ts b/src/styles/utils/index.ts
index 82602efa88b3..7fcd30bb19e2 100644
--- a/src/styles/utils/index.ts
+++ b/src/styles/utils/index.ts
@@ -1568,12 +1568,14 @@ const createStyleUtils = (theme: ThemeColors, styles: ThemeStyles) => ({
case CONST.SEARCH_TABLE_COLUMNS.MERCHANT:
case CONST.SEARCH_TABLE_COLUMNS.FROM:
case CONST.SEARCH_TABLE_COLUMNS.TO:
+ columnWidth = styles.flex1;
+ break;
case CONST.SEARCH_TABLE_COLUMNS.CATEGORY:
case CONST.SEARCH_TABLE_COLUMNS.TAG:
- columnWidth = styles.flex1;
+ columnWidth = {...getWidthStyle(variables.w36), ...styles.flex1};
break;
case CONST.SEARCH_TABLE_COLUMNS.TAX_AMOUNT:
- case CONST.SEARCH_TABLE_COLUMNS.TOTAL:
+ case CONST.SEARCH_TABLE_COLUMNS.TOTAL_AMOUNT:
columnWidth = {...getWidthStyle(variables.w96), ...styles.alignItemsEnd};
break;
case CONST.SEARCH_TABLE_COLUMNS.TYPE:
diff --git a/src/styles/variables.ts b/src/styles/variables.ts
index 6f1cac46d729..493dd993b45b 100644
--- a/src/styles/variables.ts
+++ b/src/styles/variables.ts
@@ -86,6 +86,7 @@ export default {
iconBottomBar: 24,
sidebarAvatarSize: 28,
iconHeader: 48,
+ iconSection: 68,
emojiSize: 20,
emojiLineHeight: 28,
iouAmountTextSize: 40,
@@ -190,6 +191,8 @@ export default {
eReceiptBGHeight: 540,
eReceiptBGHWidth: 335,
eReceiptTextContainerWidth: 263,
+ receiptPlaceholderIconWidth: 80,
+ receiptPlaceholderIconHeight: 80,
reportPreviewMaxWidth: 335,
reportActionImagesSingleImageHeight: 147,
reportActionImagesDoubleImageHeight: 138,
diff --git a/src/types/form/PolicyDistanceRateTaxReclaimableOnEditForm.ts b/src/types/form/PolicyDistanceRateTaxReclaimableOnEditForm.ts
new file mode 100644
index 000000000000..b41facf3604f
--- /dev/null
+++ b/src/types/form/PolicyDistanceRateTaxReclaimableOnEditForm.ts
@@ -0,0 +1,18 @@
+import type {ValueOf} from 'type-fest';
+import type Form from './Form';
+
+const INPUT_IDS = {
+ TAX_CLAIMABLE_VALUE: 'taxClaimableValue',
+} as const;
+
+type InputID = ValueOf;
+
+type PolicyDistanceRateTaxReclaimableOnEditForm = Form<
+ InputID,
+ {
+ [INPUT_IDS.TAX_CLAIMABLE_VALUE]: string;
+ }
+>;
+
+export type {PolicyDistanceRateTaxReclaimableOnEditForm};
+export default INPUT_IDS;
diff --git a/src/types/form/index.ts b/src/types/form/index.ts
index abb0147bf170..d56ab69d6a4e 100644
--- a/src/types/form/index.ts
+++ b/src/types/form/index.ts
@@ -46,6 +46,7 @@ export type {WorkspaceTaxNameForm} from './WorkspaceTaxNameForm';
export type {WorkspaceTaxValueForm} from './WorkspaceTaxValueForm';
export type {WorkspaceTaxCustomName} from './WorkspaceTaxCustomName';
export type {PolicyCreateDistanceRateForm} from './PolicyCreateDistanceRateForm';
+export type {PolicyDistanceRateTaxReclaimableOnEditForm} from './PolicyDistanceRateTaxReclaimableOnEditForm';
export type {PolicyDistanceRateEditForm} from './PolicyDistanceRateEditForm';
export type {WalletAdditionalDetailsForm} from './WalletAdditionalDetailsForm';
export type {NewChatNameForm} from './NewChatNameForm';
diff --git a/src/types/onyx/Policy.ts b/src/types/onyx/Policy.ts
index fc989dcd9783..070803e2e2fd 100644
--- a/src/types/onyx/Policy.ts
+++ b/src/types/onyx/Policy.ts
@@ -555,6 +555,7 @@ type PolicyConnectionName = ValueOf;
type PolicyConnectionSyncProgress = {
stageInProgress: PolicyConnectionSyncStage;
connectionName: PolicyConnectionName;
+ timestamp: string;
};
export default Policy;
diff --git a/tests/utils/TestHelper.ts b/tests/utils/TestHelper.ts
index 832c3e8b3336..9ca0969abc6a 100644
--- a/tests/utils/TestHelper.ts
+++ b/tests/utils/TestHelper.ts
@@ -1,4 +1,4 @@
-import Str from 'expensify-common/lib/str';
+import {Str} from 'expensify-common';
import Onyx from 'react-native-onyx';
import CONST from '@src/CONST';
import * as Session from '@src/libs/actions/Session';