diff --git a/.eslintrc.changed.js b/.eslintrc.changed.js
index a72dd6a9250a..a1c2c452273e 100644
--- a/.eslintrc.changed.js
+++ b/.eslintrc.changed.js
@@ -10,7 +10,17 @@ module.exports = {
},
overrides: [
{
- files: ['src/libs/ReportUtils.ts', 'src/libs/actions/IOU.ts', 'src/libs/actions/Report.ts', 'src/libs/actions/Task.ts'],
+ files: [
+ 'src/libs/ReportUtils.ts',
+ 'src/libs/actions/IOU.ts',
+ 'src/libs/actions/Report.ts',
+ 'src/libs/actions/Task.ts',
+ 'src/libs/OptionsListUtils.ts',
+ 'src/libs/ReportActionsUtils.ts',
+ 'src/libs/TransactionUtils/index.ts',
+ 'src/pages/home/ReportScreen.tsx',
+ 'src/pages/workspace/WorkspaceInitialPage.tsx',
+ ],
rules: {
'rulesdir/no-default-id-values': 'off',
},
diff --git a/Mobile-Expensify b/Mobile-Expensify
index 9854d5bfa2e3..43c5ef761b59 160000
--- a/Mobile-Expensify
+++ b/Mobile-Expensify
@@ -1 +1 @@
-Subproject commit 9854d5bfa2e31c702066e161256e0a9655051892
+Subproject commit 43c5ef761b59d38a297904c5917c326d86c83fb7
diff --git a/android/app/build.gradle b/android/app/build.gradle
index 5ebefd8304c7..cf34cd05f8fd 100644
--- a/android/app/build.gradle
+++ b/android/app/build.gradle
@@ -110,8 +110,8 @@ android {
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
multiDexEnabled rootProject.ext.multiDexEnabled
- versionCode 1009007702
- versionName "9.0.77-2"
+ versionCode 1009007704
+ versionName "9.0.77-4"
// Supported language variants must be declared here to avoid from being removed during the compilation.
// This also helps us to not include unnecessary language variants in the APK.
resConfigs "en", "es"
diff --git a/config/webpack/webpack.common.ts b/config/webpack/webpack.common.ts
index c60670c72324..ac086d3a9bed 100644
--- a/config/webpack/webpack.common.ts
+++ b/config/webpack/webpack.common.ts
@@ -175,7 +175,7 @@ const getCommonConfiguration = ({file = '.env', platform = 'web'}: Environment):
// We are importing this worker as a string by using asset/source otherwise it will default to loading via an HTTPS request later.
// This causes issues if we have gone offline before the pdfjs web worker is set up as we won't be able to load it from the server.
{
- test: new RegExp('node_modules/pdfjs-dist/legacy/build/pdf.worker.min.mjs'),
+ test: new RegExp('node_modules/pdfjs-dist/build/pdf.worker.min.mjs'),
type: 'asset/source',
},
diff --git a/docs/articles/expensify-classic/connections/Expensify-API.md b/docs/articles/expensify-classic/connections/Expensify-API.md
index fba85dcd154a..e2fbdbfd7703 100644
--- a/docs/articles/expensify-classic/connections/Expensify-API.md
+++ b/docs/articles/expensify-classic/connections/Expensify-API.md
@@ -11,11 +11,11 @@ To begin, review our [Integration Server Manual](https://integrations.expensify.
We've compiled answers to some frequently asked questions to help you get started.
-**Should I give your support team my API credentials when I need help?**
+## Should I give your support team my API credentials when I need help?
If you’re seeking help with Expensify's API, do not share your partnerUserSecret. If you do, immediately rotate your credentials on [this page](https://www.expensify.com/tools/integrations/).
-**Is there a rate limit?**
+## Is there a rate limit?
To keep our platform stable and handle high traffic, Expensify limits how many API requests you can send:
- Up to 5 requests every 10 seconds
@@ -23,38 +23,38 @@ To keep our platform stable and handle high traffic, Expensify limits how many A
Sending more requests than allowed may result in an error with status code `429`.
-**What is a Policy ID?**
+## What is a Policy ID?
This is also known as a Workspace ID. To find your Policy/Workspace ID,
Hover over Settings and click Workspaces.
Click the name of the Workspace.
Copy the ID number from the URL. For example, if the URL is https://www.expensify.com/policy?param={"policyID":"0810E551A5F2A9C2”}, then your workspace ID is 0810E551A5F2A9C2.
-**Can I use the parent type `file` to export workspace/policy data?**
+## Can I use the parent type `file` to export workspace/policy data?
No. The parent type `file` can only be used to export expense and report data — not policy information. To export policy data (e.g., categories, tags), you must use the `get` type with `inputSettings.type` set to `policy`.
-**Can I use the API to create Domain Groups?**
+## Can I use the API to create Domain Groups?
No, you cannot create domain groups. You can only assign users to them.
-**I’m exporting expense IDs `${expense.transactionID}` but when I open my CSV in Excel, it’s changing all the IDs and making them look the same. How can I prevent this?**
+## I’m exporting expense IDs `${expense.transactionID}` but when I open my CSV in Excel, it’s changing all the IDs and making them look the same. How can I prevent this?
Try prepending a non-numeric character like a quote to force Excel to interpret the value as a string and not a number (i.e., `'${expense.transactionID}`).
-**How can we export the person who will approve a report while the reports are still processing?**
+## How can we export the person who will approve a report while the reports are still processing?
Use the field ${report.managerEmail}.
-**Why won’t my boolean field return any data?**
+## Why won’t my boolean field return any data?
Boolean fields won't output values without a string. For example, instead of using `${expense.billable}`, use `${expense.billable?string("Yes", "No")}`. This will display "Yes" if the expense is billable and "No" if it is not.
-**Can I export the reports for just one user?**
+## Can I export the reports for just one user?
Not in a quick convenient way, as you would need to include the user in your template. The simplest approach is to export data for all users and then apply a filter in your preferred spreadsheet program.
-**Can I create expenses on behalf of users?**
+## Can I create expenses on behalf of users?
Yes. However, to access the Expense Creator API on behalf of employees, Expensify needs to verify the following setup:
@@ -63,17 +63,17 @@ Verify you have internal authorization to add data to other accounts within your
If you need this access, contact concierge@expensify.com and reference this help page.
-## Using Postman
+# Using Postman
Many customers use Postman to help them build out their APIs. Below are some guides contributed by our customers. Please note, in all cases, you will need to first generate your authentication credentials, the steps for which can be found [here](https://integrations.expensify.com/Integration-Server/doc/#introduction) and have them ready:
-### Download expenses from a report as a CSV file
+## Download expenses from a report as a CSV file
**Step 1: Get the ID of a report you want to export in Expensify**
Find the ID by opening the expense report and clicking Details at the top right corner of the page. At the top of the menu, the ID is provided as the “Long ID.”
-**Step 3: Export (generate) a "Report" as a CSV file**
+**Step 2: Export (generate) a "Report" as a CSV file**
{% include info.html %}
For this you'll use the Documentation under [Report Exporter](https://integrations.expensify.com/Integration-Server/doc/#export).
{% include end-info.html %}
@@ -146,11 +146,11 @@ The template key will have the value like below:
The template variable determines what information is saved in your CSV file. If you want more columns than merchant, amount, and transaction date, follow the syntax as defined in the export template format documentation.
-**Step 4: Save your generated file name**
+**Step 3: Save your generated file name**
-Expensify currently supports only the "onReceive":{"immediateResponse":["returnRandomFileName"]} option in step 3, so you should receive a random filename back from the API like "exportc111111d-a1a1-a1a1-a1a1-d1111111f.csv". You will need to document this filename if you plan on running the download command after this one.
+Expensify currently supports only the "onReceive":{"immediateResponse":["returnRandomFileName"]} option in step 2, so you should receive a random filename back from the API like "exportc111111d-a1a1-a1a1-a1a1-d1111111f.csv". You will need to document this filename if you plan on running the download command after this one.
-**Step 5: Download your exported report**
+**Step 4: Download your exported report**
Set up another API call in almost the same way you did before. You don't need the template key in the Body anymore, so delete that and set the Body type to "none". Then modify your requestJobDescription to read like below, but with your own credentials and file name:
@@ -170,7 +170,7 @@ Click Go and you should see the CSV in the response body.
*Thank you to our customer Frederico Pettinella who originally wrote and shared this guide.*
-### Use Advanced Employee Updater API with Postman
+## Use Advanced Employee Updater API with Postman
1. Create a new request.
2. Select POST as the method.
diff --git a/docs/articles/new-expensify/expenses-&-payments/Create-an-expense.md b/docs/articles/new-expensify/expenses-&-payments/Create-an-expense.md
index ea058df9c1b1..1b1702c6fcc7 100644
--- a/docs/articles/new-expensify/expenses-&-payments/Create-an-expense.md
+++ b/docs/articles/new-expensify/expenses-&-payments/Create-an-expense.md
@@ -51,6 +51,11 @@ When an expense is submitted to a workspace, your approver will receive an email
{% include end-selector.html %}
+![Click Global Create]({{site.url}}/assets/images/ExpensifyHelp-CreateExpense-1.png){:width="100%"}
+![Click Submit expense]({{site.url}}/assets/images/ExpensifyHelp-CreateExpense-2.png){:width="100%"}
+![Click Scan]({{site.url}}/assets/images/ExpensifyHelp-CreateExpense-3.png){:width="100%"}
+![Enter workspace or individual's name]({{site.url}}/assets/images/ExpensifyHelp-CreateExpense-4.png){:width="100%"}
+
{% include info.html %}
You can also forward receipts to receipts@expensify.com using your primary or secondary email address. SmartScan will automatically extract all the details from the receipt and add them to your expenses.
{% include end-info.html %}
diff --git a/ios/NewExpensify/Info.plist b/ios/NewExpensify/Info.plist
index fe4c07fdac9e..108706d79a0c 100644
--- a/ios/NewExpensify/Info.plist
+++ b/ios/NewExpensify/Info.plist
@@ -40,7 +40,7 @@
CFBundleVersion
- 9.0.77.2
+ 9.0.77.4
FullStory
OrgId
diff --git a/ios/NewExpensifyTests/Info.plist b/ios/NewExpensifyTests/Info.plist
index 6a92f70d678f..ea782231aaec 100644
--- a/ios/NewExpensifyTests/Info.plist
+++ b/ios/NewExpensifyTests/Info.plist
@@ -19,6 +19,6 @@
CFBundleSignature
????
CFBundleVersion
- 9.0.77.2
+ 9.0.77.4
diff --git a/ios/NotificationServiceExtension/Info.plist b/ios/NotificationServiceExtension/Info.plist
index 7ae2ed9457e8..b14e33cdde82 100644
--- a/ios/NotificationServiceExtension/Info.plist
+++ b/ios/NotificationServiceExtension/Info.plist
@@ -13,7 +13,7 @@
CFBundleShortVersionString
9.0.77
CFBundleVersion
- 9.0.77.2
+ 9.0.77.4
NSExtension
NSExtensionPointIdentifier
diff --git a/metro.config.js b/metro.config.js
index c6e4ba6bb4ec..98bea7be80ed 100644
--- a/metro.config.js
+++ b/metro.config.js
@@ -4,6 +4,7 @@ const {getDefaultConfig: getReactNativeDefaultConfig} = require('@react-native/m
const {mergeConfig} = require('@react-native/metro-config');
const defaultAssetExts = require('metro-config/src/defaults/defaults').assetExts;
const defaultSourceExts = require('metro-config/src/defaults/defaults').sourceExts;
+const {wrapWithReanimatedMetroConfig} = require('react-native-reanimated/metro-config');
require('dotenv').config();
const defaultConfig = getReactNativeDefaultConfig(__dirname);
@@ -26,4 +27,4 @@ const config = {
},
};
-module.exports = mergeConfig(defaultConfig, expoConfig, config);
+module.exports = wrapWithReanimatedMetroConfig(mergeConfig(defaultConfig, expoConfig, config));
diff --git a/package-lock.json b/package-lock.json
index 5182b78cf5e9..0a2e67f38ee2 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "new.expensify",
- "version": "9.0.77-2",
+ "version": "9.0.77-4",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "new.expensify",
- "version": "9.0.77-2",
+ "version": "9.0.77-4",
"hasInstallScript": true,
"license": "MIT",
"workspaces": [
@@ -77,7 +77,7 @@
"react-content-loader": "^7.0.0",
"react-dom": "18.3.1",
"react-error-boundary": "^4.0.11",
- "react-fast-pdf": "1.0.20",
+ "react-fast-pdf": "1.0.21",
"react-map-gl": "^7.1.3",
"react-native": "0.75.2",
"react-native-android-location-enabler": "^2.0.1",
@@ -32467,9 +32467,9 @@
}
},
"node_modules/react-fast-pdf": {
- "version": "1.0.20",
- "resolved": "https://registry.npmjs.org/react-fast-pdf/-/react-fast-pdf-1.0.20.tgz",
- "integrity": "sha512-E2PJOO5oEqi6eNPllNOlQ8y0DiLZ3AW8t+MCN7AgJPp5pY04SeDveXHWvPN0nPU4X5sRBZ7CejeYce2QMMQDyg==",
+ "version": "1.0.21",
+ "resolved": "https://registry.npmjs.org/react-fast-pdf/-/react-fast-pdf-1.0.21.tgz",
+ "integrity": "sha512-8Uuz/jPHjHqElH+aUj3ldS/Hg/NoZ5ZS/VupGzDkVJST0UiGzxkvDxxFIQuYuiaI4NGwGmqtQGGYsjJKpyWnig==",
"license": "MIT",
"dependencies": {
"react-pdf": "^9.1.1",
diff --git a/package.json b/package.json
index c67c99e28e2d..c621b583452e 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "new.expensify",
- "version": "9.0.77-2",
+ "version": "9.0.77-4",
"author": "Expensify, Inc.",
"homepage": "https://new.expensify.com",
"description": "New Expensify is the next generation of Expensify: a reimagination of payments based atop a foundation of chat.",
@@ -139,7 +139,7 @@
"react-content-loader": "^7.0.0",
"react-dom": "18.3.1",
"react-error-boundary": "^4.0.11",
- "react-fast-pdf": "1.0.20",
+ "react-fast-pdf": "1.0.21",
"react-map-gl": "^7.1.3",
"react-native": "0.75.2",
"react-native-android-location-enabler": "^2.0.1",
diff --git a/patches/react-native-draggable-flatlist+4.0.1.patch b/patches/react-native-draggable-flatlist+4.0.1.patch
index 348f1aa5de8a..a3d29b66de7a 100644
--- a/patches/react-native-draggable-flatlist+4.0.1.patch
+++ b/patches/react-native-draggable-flatlist+4.0.1.patch
@@ -12,7 +12,7 @@ index d7d98c2..2f59c7a 100644
runOnJS(onDragEnd)({
from: activeIndexAnim.value,
diff --git a/node_modules/react-native-draggable-flatlist/src/context/refContext.tsx b/node_modules/react-native-draggable-flatlist/src/context/refContext.tsx
-index ea21575..66c5eed 100644
+index ea21575..dc6b095 100644
--- a/node_modules/react-native-draggable-flatlist/src/context/refContext.tsx
+++ b/node_modules/react-native-draggable-flatlist/src/context/refContext.tsx
@@ -1,14 +1,14 @@
@@ -32,14 +32,13 @@ index ea21575..66c5eed 100644
cellDataRef: React.MutableRefObject>;
keyToIndexRef: React.MutableRefObject>;
containerRef: React.RefObject;
-@@ -54,8 +54,8 @@ function useSetupRefs({
+@@ -54,8 +54,7 @@ function useSetupRefs({
...DEFAULT_PROPS.animationConfig,
...animationConfig,
} as WithSpringConfig;
- const animationConfigRef = useRef(animConfig);
- animationConfigRef.current = animConfig;
+ const animationConfigRef = useSharedValue(animConfig);
-+ animationConfigRef.value = animConfig;
const cellDataRef = useRef(new Map());
const keyToIndexRef = useRef(new Map());
@@ -57,7 +56,7 @@ index ce4ab68..efea240 100644
return translate;
diff --git a/node_modules/react-native-draggable-flatlist/src/hooks/useOnCellActiveAnimation.ts b/node_modules/react-native-draggable-flatlist/src/hooks/useOnCellActiveAnimation.ts
-index 7c20587..857c7d0 100644
+index 7c20587..33042e9 100644
--- a/node_modules/react-native-draggable-flatlist/src/hooks/useOnCellActiveAnimation.ts
+++ b/node_modules/react-native-draggable-flatlist/src/hooks/useOnCellActiveAnimation.ts
@@ -1,8 +1,9 @@
@@ -72,18 +71,17 @@ index 7c20587..857c7d0 100644
} from "react-native-reanimated";
import { DEFAULT_ANIMATION_CONFIG } from "../constants";
import { useAnimatedValues } from "../context/animatedValueContext";
-@@ -15,8 +16,8 @@ type Params = {
+@@ -15,8 +16,7 @@ type Params = {
export function useOnCellActiveAnimation(
{ animationConfig }: Params = { animationConfig: {} }
) {
- const animationConfigRef = useRef(animationConfig);
- animationConfigRef.current = animationConfig;
+ const animationConfigRef = useSharedValue(animationConfig);
-+ animationConfigRef.value = animationConfig;
const isActive = useIsActive();
-@@ -26,7 +27,7 @@ export function useOnCellActiveAnimation(
+@@ -26,7 +26,7 @@ export function useOnCellActiveAnimation(
const toVal = isActive && isTouchActiveNative.value ? 1 : 0;
return withSpring(toVal, {
...DEFAULT_ANIMATION_CONFIG,
diff --git a/src/ONYXKEYS.ts b/src/ONYXKEYS.ts
index 2a2959f43f66..a43f1622ec9a 100755
--- a/src/ONYXKEYS.ts
+++ b/src/ONYXKEYS.ts
@@ -733,6 +733,8 @@ const ONYXKEYS = {
RULES_MAX_EXPENSE_AGE_FORM_DRAFT: 'rulesMaxExpenseAgeFormDraft',
DEBUG_DETAILS_FORM: 'debugDetailsForm',
DEBUG_DETAILS_FORM_DRAFT: 'debugDetailsFormDraft',
+ WORKSPACE_PER_DIEM_FORM: 'workspacePerDiemForm',
+ WORKSPACE_PER_DIEM_FORM_DRAFT: 'workspacePerDiemFormDraft',
},
} as const;
@@ -826,6 +828,7 @@ type OnyxFormValuesMapping = {
[ONYXKEYS.FORMS.RULES_MAX_EXPENSE_AGE_FORM]: FormTypes.RulesMaxExpenseAgeForm;
[ONYXKEYS.FORMS.SEARCH_SAVED_SEARCH_RENAME_FORM]: FormTypes.SearchSavedSearchRenameForm;
[ONYXKEYS.FORMS.DEBUG_DETAILS_FORM]: FormTypes.DebugReportForm | FormTypes.DebugReportActionForm | FormTypes.DebugTransactionForm | FormTypes.DebugTransactionViolationForm;
+ [ONYXKEYS.FORMS.WORKSPACE_PER_DIEM_FORM]: FormTypes.WorkspacePerDiemForm;
};
type OnyxFormDraftValuesMapping = {
diff --git a/src/ROUTES.ts b/src/ROUTES.ts
index f48a5cae92f0..58d28a46a7b8 100644
--- a/src/ROUTES.ts
+++ b/src/ROUTES.ts
@@ -1325,6 +1325,26 @@ const ROUTES = {
route: 'settings/workspaces/:policyID/per-diem/settings',
getRoute: (policyID: string) => `settings/workspaces/${policyID}/per-diem/settings` as const,
},
+ WORKSPACE_PER_DIEM_DETAILS: {
+ route: 'settings/workspaces/:policyID/per-diem/details/:rateID/:subRateID',
+ getRoute: (policyID: string, rateID: string, subRateID: string) => `settings/workspaces/${policyID}/per-diem/details/${rateID}/${subRateID}` as const,
+ },
+ WORKSPACE_PER_DIEM_EDIT_DESTINATION: {
+ route: 'settings/workspaces/:policyID/per-diem/edit/destination/:rateID/:subRateID',
+ getRoute: (policyID: string, rateID: string, subRateID: string) => `settings/workspaces/${policyID}/per-diem/edit/destination/${rateID}/${subRateID}` as const,
+ },
+ WORKSPACE_PER_DIEM_EDIT_SUBRATE: {
+ route: 'settings/workspaces/:policyID/per-diem/edit/subrate/:rateID/:subRateID',
+ getRoute: (policyID: string, rateID: string, subRateID: string) => `settings/workspaces/${policyID}/per-diem/edit/subrate/${rateID}/${subRateID}` as const,
+ },
+ WORKSPACE_PER_DIEM_EDIT_AMOUNT: {
+ route: 'settings/workspaces/:policyID/per-diem/edit/amount/:rateID/:subRateID',
+ getRoute: (policyID: string, rateID: string, subRateID: string) => `settings/workspaces/${policyID}/per-diem/edit/amount/${rateID}/${subRateID}` as const,
+ },
+ WORKSPACE_PER_DIEM_EDIT_CURRENCY: {
+ route: 'settings/workspaces/:policyID/per-diem/edit/currency/:rateID/:subRateID',
+ getRoute: (policyID: string, rateID: string, subRateID: string) => `settings/workspaces/${policyID}/per-diem/edit/currency/${rateID}/${subRateID}` as const,
+ },
RULES_CUSTOM_NAME: {
route: 'settings/workspaces/:policyID/rules/name',
getRoute: (policyID: string) => `settings/workspaces/${policyID}/rules/name` as const,
diff --git a/src/SCREENS.ts b/src/SCREENS.ts
index 86c71e182a2b..6274be1044b4 100644
--- a/src/SCREENS.ts
+++ b/src/SCREENS.ts
@@ -558,6 +558,11 @@ const SCREENS = {
PER_DIEM_IMPORT: 'Per_Diem_Import',
PER_DIEM_IMPORTED: 'Per_Diem_Imported',
PER_DIEM_SETTINGS: 'Per_Diem_Settings',
+ PER_DIEM_DETAILS: 'Per_Diem_Details',
+ PER_DIEM_EDIT_DESTINATION: 'Per_Diem_Edit_Destination',
+ PER_DIEM_EDIT_SUBRATE: 'Per_Diem_Edit_Subrate',
+ PER_DIEM_EDIT_AMOUNT: 'Per_Diem_Edit_Amount',
+ PER_DIEM_EDIT_CURRENCY: 'Per_Diem_Edit_Currency',
},
EDIT_REQUEST: {
diff --git a/src/components/AmountWithoutCurrencyForm.tsx b/src/components/AmountWithoutCurrencyForm.tsx
index 0ac410013214..de65f40b3b4f 100644
--- a/src/components/AmountWithoutCurrencyForm.tsx
+++ b/src/components/AmountWithoutCurrencyForm.tsx
@@ -12,10 +12,13 @@ type AmountFormProps = {
/** Callback to update the amount in the FormProvider */
onInputChange?: (value: string) => void;
+
+ /** Should we allow negative number as valid input */
+ shouldAllowNegative?: boolean;
} & Partial;
function AmountWithoutCurrencyForm(
- {value: amount, onInputChange, inputID, name, defaultValue, accessibilityLabel, role, label, ...rest}: AmountFormProps,
+ {value: amount, onInputChange, shouldAllowNegative = false, inputID, name, defaultValue, accessibilityLabel, role, label, ...rest}: AmountFormProps,
ref: ForwardedRef,
) {
const {toLocaleDigit} = useLocalize();
@@ -32,13 +35,13 @@ function AmountWithoutCurrencyForm(
// More info: https://github.com/Expensify/App/issues/16974
const newAmountWithoutSpaces = stripSpacesFromAmount(newAmount);
const replacedCommasAmount = replaceCommasWithPeriod(newAmountWithoutSpaces);
- const withLeadingZero = addLeadingZero(replacedCommasAmount);
- if (!validateAmount(withLeadingZero, 2)) {
+ const withLeadingZero = addLeadingZero(replacedCommasAmount, shouldAllowNegative);
+ if (!validateAmount(withLeadingZero, 2, CONST.IOU.AMOUNT_MAX_LENGTH, shouldAllowNegative)) {
return;
}
onInputChange?.(withLeadingZero);
},
- [onInputChange],
+ [onInputChange, shouldAllowNegative],
);
const formattedAmount = replaceAllDigits(currentAmount, toLocaleDigit);
@@ -54,7 +57,7 @@ function AmountWithoutCurrencyForm(
accessibilityLabel={accessibilityLabel}
role={role}
ref={ref}
- keyboardType={CONST.KEYBOARD_TYPE.DECIMAL_PAD}
+ keyboardType={!shouldAllowNegative ? CONST.KEYBOARD_TYPE.DECIMAL_PAD : undefined}
// On android autoCapitalize="words" is necessary when keyboardType="decimal-pad" or inputMode="decimal" to prevent input lag.
// See https://github.com/Expensify/App/issues/51868 for more information
autoCapitalize="words"
diff --git a/src/components/CategoryPicker.tsx b/src/components/CategoryPicker.tsx
index 19bb98bff58e..f8e9e836c736 100644
--- a/src/components/CategoryPicker.tsx
+++ b/src/components/CategoryPicker.tsx
@@ -14,7 +14,7 @@ import RadioListItem from './SelectionList/RadioListItem';
import type {ListItem} from './SelectionList/types';
type CategoryPickerProps = {
- policyID: string;
+ policyID: string | undefined;
selectedCategory?: string;
onSubmit: (item: ListItem) => void;
};
diff --git a/src/components/Composer/implementation/index.native.tsx b/src/components/Composer/implementation/index.native.tsx
index cea339de07e2..0cddb32f5aeb 100644
--- a/src/components/Composer/implementation/index.native.tsx
+++ b/src/components/Composer/implementation/index.native.tsx
@@ -16,6 +16,7 @@ import useTheme from '@hooks/useTheme';
import useThemeStyles from '@hooks/useThemeStyles';
import * as EmojiUtils from '@libs/EmojiUtils';
import * as FileUtils from '@libs/fileDownload/FileUtils';
+import getPlatform from '@libs/getPlatform';
import CONST from '@src/CONST';
const excludeNoStyles: Array = [];
@@ -140,7 +141,11 @@ function Composer(
textAlignVertical="center"
style={[composerStyle, maxHeightStyle]}
markdownStyle={markdownStyle}
- autoFocus={autoFocus}
+ // /*
+ // There are cases in hybird app on android that screen goes up when there is autofocus on keyboard. (e.g. https://github.com/Expensify/App/issues/53185)
+ // Workaround for this issue is to maunally focus keyboard after it's acutally rendered which is done by useAutoFocusInput hook.
+ // */
+ autoFocus={getPlatform() !== 'android' ? autoFocus : false}
/* eslint-disable-next-line react/jsx-props-no-spreading */
{...props}
readOnly={isDisabled}
diff --git a/src/components/Composer/implementation/index.tsx b/src/components/Composer/implementation/index.tsx
index 98ac9e00a98a..5af76a2406b5 100755
--- a/src/components/Composer/implementation/index.tsx
+++ b/src/components/Composer/implementation/index.tsx
@@ -1,4 +1,5 @@
import type {MarkdownStyle} from '@expensify/react-native-live-markdown';
+import {useIsFocused} from '@react-navigation/native';
import lodashDebounce from 'lodash/debounce';
import type {BaseSyntheticEvent, ForwardedRef} from 'react';
import React, {useCallback, useEffect, useImperativeHandle, useMemo, useRef, useState} from 'react';
@@ -252,7 +253,8 @@ function Composer(
// eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps
}, [isComposerFullSize]);
- useHtmlPaste(textInput, handlePaste, true);
+ const isActive = useIsFocused();
+ useHtmlPaste(textInput, handlePaste, isActive);
useEffect(() => {
setIsRendered(true);
diff --git a/src/components/EmptySelectionListContent.tsx b/src/components/EmptySelectionListContent.tsx
index a5ac2c84eb2b..67a9a2fc83f3 100644
--- a/src/components/EmptySelectionListContent.tsx
+++ b/src/components/EmptySelectionListContent.tsx
@@ -7,6 +7,7 @@ import variables from '@styles/variables';
import CONST from '@src/CONST';
import BlockingView from './BlockingViews/BlockingView';
import * as Illustrations from './Icon/Illustrations';
+import ScrollView from './ScrollView';
import Text from './Text';
import TextLink from './TextLink';
@@ -39,17 +40,19 @@ function EmptySelectionListContent({contentType}: EmptySelectionListContentProps
);
return (
-
-
-
+
+
+
+
+
);
}
diff --git a/src/components/MoneyRequestConfirmationList.tsx b/src/components/MoneyRequestConfirmationList.tsx
index 19af05a1581b..00965d197937 100755
--- a/src/components/MoneyRequestConfirmationList.tsx
+++ b/src/components/MoneyRequestConfirmationList.tsx
@@ -177,14 +177,14 @@ function MoneyRequestConfirmationList({
shouldPlaySound = true,
isConfirmed,
}: MoneyRequestConfirmationListProps) {
- const [policyCategoriesReal] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${policyID ?? '-1'}`);
- const [policyTags] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY_TAGS}${policyID ?? '-1'}`);
- const [policyReal] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY}${policyID ?? '-1'}`);
- const [policyDraft] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY_DRAFTS}${policyID ?? '-1'}`);
- const [defaultMileageRate] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY_DRAFTS}${policyID ?? '-1'}`, {
+ const [policyCategoriesReal] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${policyID}`);
+ const [policyTags] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY_TAGS}${policyID}`);
+ const [policyReal] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY}${policyID}`);
+ const [policyDraft] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY_DRAFTS}${policyID}`);
+ const [defaultMileageRate] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY_DRAFTS}${policyID}`, {
selector: (selectedPolicy) => DistanceRequestUtils.getDefaultMileageRate(selectedPolicy),
});
- const [policyCategoriesDraft] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY_CATEGORIES_DRAFT}${policyID ?? '-1'}`);
+ const [policyCategoriesDraft] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY_CATEGORIES_DRAFT}${policyID}`);
const [lastSelectedDistanceRates] = useOnyx(ONYXKEYS.NVP_LAST_SELECTED_DISTANCE_RATES);
const [currencyList] = useOnyx(ONYXKEYS.CURRENCY_LIST);
@@ -202,17 +202,22 @@ function MoneyRequestConfirmationList({
const isTypeInvoice = iouType === CONST.IOU.TYPE.INVOICE;
const isScanRequest = useMemo(() => TransactionUtils.isScanRequest(transaction), [transaction]);
- const transactionID = transaction?.transactionID ?? '-1';
- const customUnitRateID = TransactionUtils.getRateID(transaction) ?? '-1';
+ const transactionID = transaction?.transactionID;
+ const customUnitRateID = TransactionUtils.getRateID(transaction);
useEffect(() => {
- if ((customUnitRateID && customUnitRateID !== '-1') || !isDistanceRequest) {
+ if (customUnitRateID !== '-1' || !isDistanceRequest || !transactionID || !policy?.id) {
return;
}
- const defaultRate = defaultMileageRate?.customUnitRateID ?? '';
- const lastSelectedRate = lastSelectedDistanceRates?.[policy?.id ?? ''] ?? defaultRate;
+ const defaultRate = defaultMileageRate?.customUnitRateID;
+ const lastSelectedRate = lastSelectedDistanceRates?.[policy.id] ?? defaultRate;
const rateID = lastSelectedRate;
+
+ if (!rateID) {
+ return;
+ }
+
IOU.setCustomUnitRateID(transactionID, rateID);
}, [defaultMileageRate, customUnitRateID, lastSelectedDistanceRates, policy?.id, transactionID, isDistanceRequest]);
@@ -242,6 +247,7 @@ function MoneyRequestConfirmationList({
if (
!shouldShowTax ||
!transaction ||
+ !transactionID ||
(transaction.taxCode &&
previousTransactionModifiedCurrency === transaction.modifiedCurrency &&
previousTransactionCurrency === transaction.currency &&
@@ -296,7 +302,12 @@ function MoneyRequestConfirmationList({
return true;
}
- if (!participant.isInvoiceRoom && !participant.isPolicyExpenseChat && !participant.isSelfDM && ReportUtils.isOptimisticPersonalDetail(participant.accountID ?? -1)) {
+ if (
+ !participant.isInvoiceRoom &&
+ !participant.isPolicyExpenseChat &&
+ !participant.isSelfDM &&
+ ReportUtils.isOptimisticPersonalDetail(participant.accountID ?? CONST.DEFAULT_NUMBER_ID)
+ ) {
return true;
}
@@ -325,7 +336,7 @@ function MoneyRequestConfirmationList({
if (isFirstUpdatedDistanceAmount.current) {
return;
}
- if (!isDistanceRequest) {
+ if (!isDistanceRequest || !transactionID) {
return;
}
const amount = DistanceRequestUtils.getDistanceRequestAmount(distance, unit ?? CONST.CUSTOM_UNITS.DISTANCE_UNIT_MILES, rate ?? 0);
@@ -334,7 +345,7 @@ function MoneyRequestConfirmationList({
}, [distance, rate, unit, transactionID, currency, isDistanceRequest]);
useEffect(() => {
- if (!shouldCalculateDistanceAmount) {
+ if (!shouldCalculateDistanceAmount || !transactionID) {
return;
}
@@ -342,7 +353,7 @@ function MoneyRequestConfirmationList({
IOU.setMoneyRequestAmount(transactionID, amount, currency ?? '');
// If it's a split request among individuals, set the split shares
- const participantAccountIDs: number[] = selectedParticipantsProp.map((participant) => participant.accountID ?? -1);
+ const participantAccountIDs: number[] = selectedParticipantsProp.map((participant) => participant.accountID ?? CONST.DEFAULT_NUMBER_ID);
if (isTypeSplit && !isPolicyExpenseChat && amount && transaction?.currency) {
IOU.setSplitShares(transaction, amount, currency, participantAccountIDs);
}
@@ -364,20 +375,25 @@ function MoneyRequestConfirmationList({
return;
}
- let taxableAmount: number;
- let taxCode: string;
+ let taxableAmount: number | undefined;
+ let taxCode: string | undefined;
if (isDistanceRequest) {
- const customUnitRate = getDistanceRateCustomUnitRate(policy, customUnitRateID);
- taxCode = customUnitRate?.attributes?.taxRateExternalID ?? '';
- taxableAmount = DistanceRequestUtils.getTaxableAmount(policy, customUnitRateID, distance);
+ if (customUnitRateID) {
+ const customUnitRate = getDistanceRateCustomUnitRate(policy, customUnitRateID);
+ taxCode = customUnitRate?.attributes?.taxRateExternalID;
+ taxableAmount = DistanceRequestUtils.getTaxableAmount(policy, customUnitRateID, distance);
+ }
} else {
taxableAmount = transaction.amount ?? 0;
taxCode = transaction.taxCode ?? TransactionUtils.getDefaultTaxCode(policy, transaction) ?? '';
}
- const taxPercentage = TransactionUtils.getTaxValue(policy, transaction, taxCode) ?? '';
- const taxAmount = TransactionUtils.calculateTaxAmount(taxPercentage, taxableAmount, transaction.currency);
- const taxAmountInSmallestCurrencyUnits = CurrencyUtils.convertToBackendAmount(Number.parseFloat(taxAmount.toString()));
- IOU.setMoneyRequestTaxAmount(transaction.transactionID ?? '', taxAmountInSmallestCurrencyUnits);
+
+ if (taxCode && taxableAmount) {
+ const taxPercentage = TransactionUtils.getTaxValue(policy, transaction, taxCode) ?? '';
+ const taxAmount = TransactionUtils.calculateTaxAmount(taxPercentage, taxableAmount, transaction.currency);
+ const taxAmountInSmallestCurrencyUnits = CurrencyUtils.convertToBackendAmount(Number.parseFloat(taxAmount.toString()));
+ IOU.setMoneyRequestTaxAmount(transaction.transactionID, taxAmountInSmallestCurrencyUnits);
+ }
}, [
policy,
shouldShowTax,
@@ -522,7 +538,7 @@ function MoneyRequestConfirmationList({
rightElement: (
onSplitShareChange(participantOption.accountID ?? -1, Number(value))}
+ onAmountChange={(value: string) => onSplitShareChange(participantOption.accountID ?? CONST.DEFAULT_NUMBER_ID, Number(value))}
maxLength={formattedTotalAmount.length}
contentWidth={formattedTotalAmount.length * 8}
/>
@@ -637,7 +653,7 @@ function MoneyRequestConfirmationList({
}, [isTypeSplit, translate, payeePersonalDetails, getSplitSectionHeader, splitParticipants, selectedParticipants]);
useEffect(() => {
- if (!isDistanceRequest || isMovingTransactionFromTrackExpense) {
+ if (!isDistanceRequest || isMovingTransactionFromTrackExpense || !transactionID) {
return;
}
@@ -669,16 +685,20 @@ function MoneyRequestConfirmationList({
// Auto select the category if there is only one enabled category and it is required
useEffect(() => {
const enabledCategories = Object.values(policyCategories ?? {}).filter((category) => category.enabled);
- if (iouCategory || !shouldShowCategories || enabledCategories.length !== 1 || !isCategoryRequired) {
+ if (!transactionID || iouCategory || !shouldShowCategories || enabledCategories.length !== 1 || !isCategoryRequired) {
return;
}
- IOU.setMoneyRequestCategory(transactionID, enabledCategories.at(0)?.name ?? '');
+ IOU.setMoneyRequestCategory(transactionID, enabledCategories.at(0)?.name ?? '', policy?.id);
// Keep 'transaction' out to ensure that we autoselect the option only once
// eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps
- }, [shouldShowCategories, policyCategories, isCategoryRequired]);
+ }, [shouldShowCategories, policyCategories, isCategoryRequired, policy?.id]);
// Auto select the tag if there is only one enabled tag and it is required
useEffect(() => {
+ if (!transactionID) {
+ return;
+ }
+
let updatedTagsString = TransactionUtils.getTag(transaction);
policyTagLists.forEach((tagList, index) => {
const isTagListRequired = tagList.required ?? false;
@@ -721,7 +741,7 @@ function MoneyRequestConfirmationList({
*/
const confirm = useCallback(
(paymentMethod: PaymentMethodType | undefined) => {
- if (routeError) {
+ if (!!routeError || !transactionID) {
return;
}
if (iouType === CONST.IOU.TYPE.INVOICE && !hasInvoicingDetails(policy)) {
diff --git a/src/components/MoneyRequestConfirmationListFooter.tsx b/src/components/MoneyRequestConfirmationListFooter.tsx
index e32c4eae410f..51cb2a6d6f39 100644
--- a/src/components/MoneyRequestConfirmationListFooter.tsx
+++ b/src/components/MoneyRequestConfirmationListFooter.tsx
@@ -163,7 +163,7 @@ type MoneyRequestConfirmationListFooterProps = {
transaction: OnyxEntry;
/** The transaction ID */
- transactionID: string;
+ transactionID: string | undefined;
/** The unit */
unit: Unit | undefined;
@@ -295,7 +295,7 @@ function MoneyRequestConfirmationListFooter({
description={translate('iou.amount')}
interactive={!isReadOnly}
onPress={() => {
- if (isDistanceRequest) {
+ if (isDistanceRequest || !transactionID) {
return;
}
@@ -326,6 +326,10 @@ function MoneyRequestConfirmationListFooter({
title={iouComment}
description={translate('common.description')}
onPress={() => {
+ if (!transactionID) {
+ return;
+ }
+
Navigation.navigate(ROUTES.MONEY_REQUEST_STEP_DESCRIPTION.getRoute(action, iouType, transactionID, reportID, Navigation.getActiveRoute(), reportActionID));
}}
style={[styles.moneyRequestMenuItem]}
@@ -349,7 +353,13 @@ function MoneyRequestConfirmationListFooter({
description={translate('common.distance')}
style={[styles.moneyRequestMenuItem]}
titleStyle={styles.flex1}
- onPress={() => Navigation.navigate(ROUTES.MONEY_REQUEST_STEP_DISTANCE.getRoute(action, iouType, transactionID, reportID, Navigation.getActiveRouteWithoutParams()))}
+ onPress={() => {
+ if (!transactionID) {
+ return;
+ }
+
+ Navigation.navigate(ROUTES.MONEY_REQUEST_STEP_DISTANCE.getRoute(action, iouType, transactionID, reportID, Navigation.getActiveRouteWithoutParams()));
+ }}
disabled={didConfirm}
interactive={!isReadOnly}
/>
@@ -366,7 +376,13 @@ function MoneyRequestConfirmationListFooter({
description={translate('common.rate')}
style={[styles.moneyRequestMenuItem]}
titleStyle={styles.flex1}
- onPress={() => Navigation.navigate(ROUTES.MONEY_REQUEST_STEP_DISTANCE_RATE.getRoute(action, iouType, transactionID, reportID, Navigation.getActiveRouteWithoutParams()))}
+ onPress={() => {
+ if (!transactionID) {
+ return;
+ }
+
+ Navigation.navigate(ROUTES.MONEY_REQUEST_STEP_DISTANCE_RATE.getRoute(action, iouType, transactionID, reportID, Navigation.getActiveRouteWithoutParams()));
+ }}
disabled={didConfirm}
interactive={!!rate && !isReadOnly && isPolicyExpenseChat}
/>
@@ -384,6 +400,10 @@ function MoneyRequestConfirmationListFooter({
style={[styles.moneyRequestMenuItem]}
titleStyle={styles.flex1}
onPress={() => {
+ if (!transactionID) {
+ return;
+ }
+
Navigation.navigate(ROUTES.MONEY_REQUEST_STEP_MERCHANT.getRoute(action, iouType, transactionID, reportID, Navigation.getActiveRoute()));
}}
disabled={didConfirm}
@@ -408,6 +428,10 @@ function MoneyRequestConfirmationListFooter({
style={[styles.moneyRequestMenuItem]}
titleStyle={styles.flex1}
onPress={() => {
+ if (!transactionID) {
+ return;
+ }
+
Navigation.navigate(ROUTES.MONEY_REQUEST_STEP_DATE.getRoute(action, iouType, transactionID, reportID, Navigation.getActiveRoute(), reportActionID));
}}
disabled={didConfirm}
@@ -427,12 +451,16 @@ function MoneyRequestConfirmationListFooter({
title={iouCategory}
description={translate('common.category')}
numberOfLinesTitle={2}
- onPress={() =>
+ onPress={() => {
+ if (!transactionID) {
+ return;
+ }
+
Navigation.navigate(
ROUTES.MONEY_REQUEST_STEP_CATEGORY.getRoute(action, iouType, transactionID, reportID, Navigation.getActiveRoute(), reportActionID),
CONST.NAVIGATION.ACTION_TYPE.PUSH,
- )
- }
+ );
+ }}
style={[styles.moneyRequestMenuItem]}
titleStyle={styles.flex1}
disabled={didConfirm}
@@ -454,9 +482,13 @@ function MoneyRequestConfirmationListFooter({
title={TransactionUtils.getTagForDisplay(transaction, index)}
description={name}
numberOfLinesTitle={2}
- onPress={() =>
- Navigation.navigate(ROUTES.MONEY_REQUEST_STEP_TAG.getRoute(action, iouType, index, transactionID, reportID, Navigation.getActiveRoute(), reportActionID))
- }
+ onPress={() => {
+ if (!transactionID) {
+ return;
+ }
+
+ Navigation.navigate(ROUTES.MONEY_REQUEST_STEP_TAG.getRoute(action, iouType, index, transactionID, reportID, Navigation.getActiveRoute(), reportActionID));
+ }}
style={[styles.moneyRequestMenuItem]}
disabled={didConfirm}
interactive={!isReadOnly}
@@ -476,7 +508,13 @@ function MoneyRequestConfirmationListFooter({
description={taxRates?.name}
style={[styles.moneyRequestMenuItem]}
titleStyle={styles.flex1}
- onPress={() => Navigation.navigate(ROUTES.MONEY_REQUEST_STEP_TAX_RATE.getRoute(action, iouType, transactionID, reportID, Navigation.getActiveRoute()))}
+ onPress={() => {
+ if (!transactionID) {
+ return;
+ }
+
+ Navigation.navigate(ROUTES.MONEY_REQUEST_STEP_TAX_RATE.getRoute(action, iouType, transactionID, reportID, Navigation.getActiveRoute()));
+ }}
disabled={didConfirm}
interactive={canModifyTaxFields}
/>
@@ -493,7 +531,13 @@ function MoneyRequestConfirmationListFooter({
description={translate('iou.taxAmount')}
style={[styles.moneyRequestMenuItem]}
titleStyle={styles.flex1}
- onPress={() => Navigation.navigate(ROUTES.MONEY_REQUEST_STEP_TAX_AMOUNT.getRoute(action, iouType, transactionID, reportID, Navigation.getActiveRoute()))}
+ onPress={() => {
+ if (!transactionID) {
+ return;
+ }
+
+ Navigation.navigate(ROUTES.MONEY_REQUEST_STEP_TAX_AMOUNT.getRoute(action, iouType, transactionID, reportID, Navigation.getActiveRoute()));
+ }}
disabled={didConfirm}
interactive={canModifyTaxFields}
/>
@@ -512,7 +556,13 @@ function MoneyRequestConfirmationListFooter({
}`}
style={[styles.moneyRequestMenuItem]}
titleStyle={styles.flex1}
- onPress={() => Navigation.navigate(ROUTES.MONEY_REQUEST_ATTENDEE.getRoute(action, iouType, transactionID, reportID, Navigation.getActiveRouteWithoutParams()))}
+ onPress={() => {
+ if (!transactionID) {
+ return;
+ }
+
+ Navigation.navigate(ROUTES.MONEY_REQUEST_ATTENDEE.getRoute(action, iouType, transactionID, reportID, Navigation.getActiveRouteWithoutParams()));
+ }}
interactive
shouldRenderAsHTML
/>
@@ -557,7 +607,13 @@ function MoneyRequestConfirmationListFooter({
{isLocalFile && Str.isPDF(receiptFilename) ? (
Navigation.navigate(ROUTES.TRANSACTION_RECEIPT.getRoute(reportID ?? '', transactionID ?? ''))}
+ onPress={() => {
+ if (!transactionID) {
+ return;
+ }
+
+ Navigation.navigate(ROUTES.TRANSACTION_RECEIPT.getRoute(reportID, transactionID));
+ }}
accessibilityRole={CONST.ROLE.BUTTON}
accessibilityLabel={translate('accessibilityHints.viewAttachment')}
disabled={!shouldDisplayReceipt}
@@ -570,7 +626,13 @@ function MoneyRequestConfirmationListFooter({
) : (
Navigation.navigate(ROUTES.TRANSACTION_RECEIPT.getRoute(reportID ?? '', transactionID ?? ''))}
+ onPress={() => {
+ if (!transactionID) {
+ return;
+ }
+
+ Navigation.navigate(ROUTES.TRANSACTION_RECEIPT.getRoute(reportID, transactionID));
+ }}
disabled={!shouldDisplayReceipt || isThumbnail}
accessibilityRole={CONST.ROLE.BUTTON}
accessibilityLabel={translate('accessibilityHints.viewAttachment')}
@@ -625,7 +687,10 @@ function MoneyRequestConfirmationListFooter({
isLabelHoverable={false}
interactive={!isReadOnly && canUpdateSenderWorkspace}
onPress={() => {
- Navigation.navigate(ROUTES.MONEY_REQUEST_STEP_SEND_FROM.getRoute(iouType, transaction?.transactionID ?? '-1', reportID, Navigation.getActiveRouteWithoutParams()));
+ if (!transaction?.transactionID) {
+ return;
+ }
+ Navigation.navigate(ROUTES.MONEY_REQUEST_STEP_SEND_FROM.getRoute(iouType, transaction?.transactionID, reportID, Navigation.getActiveRouteWithoutParams()));
}}
style={styles.moneyRequestMenuItem}
labelStyle={styles.mt2}
@@ -644,11 +709,15 @@ function MoneyRequestConfirmationListFooter({
? receiptThumbnailContent
: shouldShowReceiptEmptyState && (
+ onPress={() => {
+ if (!transactionID) {
+ return;
+ }
+
Navigation.navigate(
ROUTES.MONEY_REQUEST_STEP_SCAN.getRoute(CONST.IOU.ACTION.CREATE, iouType, transactionID, reportID, Navigation.getActiveRouteWithoutParams()),
- )
- }
+ );
+ }}
/>
))}
{primaryFields}
diff --git a/src/components/PDFThumbnail/index.tsx b/src/components/PDFThumbnail/index.tsx
index 495c14ff76e1..6b83afe603c1 100644
--- a/src/components/PDFThumbnail/index.tsx
+++ b/src/components/PDFThumbnail/index.tsx
@@ -1,6 +1,6 @@
import 'core-js/proposals/promise-with-resolvers';
// eslint-disable-next-line import/extensions
-import pdfWorkerSource from 'pdfjs-dist/legacy/build/pdf.worker.min.mjs';
+import pdfWorkerSource from 'pdfjs-dist/build/pdf.worker.min.mjs';
import React, {useMemo, useState} from 'react';
import {View} from 'react-native';
import {Document, pdfjs, Thumbnail} from 'react-pdf';
diff --git a/src/components/TextInput/BaseTextInput/index.native.tsx b/src/components/TextInput/BaseTextInput/index.native.tsx
index cdfcb22f1f2c..605fb284bf24 100644
--- a/src/components/TextInput/BaseTextInput/index.native.tsx
+++ b/src/components/TextInput/BaseTextInput/index.native.tsx
@@ -17,6 +17,7 @@ import Text from '@components/Text';
import * as styleConst from '@components/TextInput/styleConst';
import TextInputClearButton from '@components/TextInput/TextInputClearButton';
import TextInputLabel from '@components/TextInput/TextInputLabel';
+import useHtmlPaste from '@hooks/useHtmlPaste';
import useLocalize from '@hooks/useLocalize';
import useMarkdownStyle from '@hooks/useMarkdownStyle';
import useStyleUtils from '@hooks/useStyleUtils';
@@ -99,6 +100,8 @@ function BaseTextInput(
const input = useRef(null);
const isLabelActive = useRef(initialActiveLabel);
+ useHtmlPaste(input, undefined, isMarkdownEnabled);
+
// AutoFocus which only works on mount:
useEffect(() => {
// We are manually managing focus to prevent this issue: https://github.com/Expensify/App/issues/4514
diff --git a/src/components/TextInput/BaseTextInput/index.tsx b/src/components/TextInput/BaseTextInput/index.tsx
index 00675ca4ccd6..45aa868ad219 100644
--- a/src/components/TextInput/BaseTextInput/index.tsx
+++ b/src/components/TextInput/BaseTextInput/index.tsx
@@ -1,7 +1,7 @@
import {Str} from 'expensify-common';
-import type {ForwardedRef} from 'react';
+import type {ForwardedRef, MutableRefObject} from 'react';
import React, {forwardRef, useCallback, useEffect, useMemo, useRef, useState} from 'react';
-import type {GestureResponderEvent, LayoutChangeEvent, NativeSyntheticEvent, StyleProp, TextInputFocusEventData, ViewStyle} from 'react-native';
+import type {GestureResponderEvent, LayoutChangeEvent, NativeSyntheticEvent, StyleProp, TextInput, TextInputFocusEventData, ViewStyle} from 'react-native';
import {ActivityIndicator, StyleSheet, View} from 'react-native';
import {useSharedValue, withSpring} from 'react-native-reanimated';
import Checkbox from '@components/Checkbox';
@@ -18,6 +18,7 @@ import Text from '@components/Text';
import * as styleConst from '@components/TextInput/styleConst';
import TextInputClearButton from '@components/TextInput/TextInputClearButton';
import TextInputLabel from '@components/TextInput/TextInputLabel';
+import useHtmlPaste from '@hooks/useHtmlPaste';
import useLocalize from '@hooks/useLocalize';
import useMarkdownStyle from '@hooks/useMarkdownStyle';
import useStyleUtils from '@hooks/useStyleUtils';
@@ -107,6 +108,8 @@ function BaseTextInput(
const isLabelActive = useRef(initialActiveLabel);
const didScrollToEndRef = useRef(false);
+ useHtmlPaste(input as MutableRefObject, undefined, isMarkdownEnabled);
+
// AutoFocus which only works on mount:
useEffect(() => {
// We are manually managing focus to prevent this issue: https://github.com/Expensify/App/issues/4514
diff --git a/src/hooks/useCleanupSelectedOptions/index.ts b/src/hooks/useCleanupSelectedOptions/index.ts
new file mode 100644
index 000000000000..7451e85aef23
--- /dev/null
+++ b/src/hooks/useCleanupSelectedOptions/index.ts
@@ -0,0 +1,21 @@
+import {NavigationContainerRefContext, useIsFocused} from '@react-navigation/native';
+import {useContext, useEffect} from 'react';
+import NAVIGATORS from '@src/NAVIGATORS';
+
+const useCleanupSelectedOptions = (cleanupFunction?: () => void) => {
+ const navigationContainerRef = useContext(NavigationContainerRefContext);
+ const state = navigationContainerRef?.getState();
+ const lastRoute = state?.routes.at(-1);
+ const isRightModalOpening = lastRoute?.name === NAVIGATORS.RIGHT_MODAL_NAVIGATOR;
+
+ const isFocused = useIsFocused();
+
+ useEffect(() => {
+ if (isFocused || isRightModalOpening) {
+ return;
+ }
+ cleanupFunction?.();
+ }, [isFocused, cleanupFunction, isRightModalOpening]);
+};
+
+export default useCleanupSelectedOptions;
diff --git a/src/hooks/useHtmlPaste/index.ts b/src/hooks/useHtmlPaste/index.ts
index ebffbf1b54b6..1a7e62f3141e 100644
--- a/src/hooks/useHtmlPaste/index.ts
+++ b/src/hooks/useHtmlPaste/index.ts
@@ -1,4 +1,3 @@
-import {useNavigation} from '@react-navigation/native';
import {useCallback, useEffect} from 'react';
import Parser from '@libs/Parser';
import CONST from '@src/CONST';
@@ -38,9 +37,7 @@ const insertAtCaret = (target: HTMLElement, insertedText: string, maxLength: num
}
};
-const useHtmlPaste: UseHtmlPaste = (textInputRef, preHtmlPasteCallback, removeListenerOnScreenBlur = false, maxLength = CONST.MAX_COMMENT_LENGTH + 1) => {
- const navigation = useNavigation();
-
+const useHtmlPaste: UseHtmlPaste = (textInputRef, preHtmlPasteCallback, isActive = false, maxLength = CONST.MAX_COMMENT_LENGTH + 1) => {
/**
* Set pasted text to clipboard
* @param {String} text
@@ -145,27 +142,16 @@ const useHtmlPaste: UseHtmlPaste = (textInputRef, preHtmlPasteCallback, removeLi
);
useEffect(() => {
- // we need to re-register listener on navigation focus/blur if the component (like Composer) is not unmounting
- // when navigating away to different screen (report) to avoid paste event on other screen being wrongly handled
- // by current screen paste listener
- let unsubscribeFocus: () => void;
- let unsubscribeBlur: () => void;
- if (removeListenerOnScreenBlur) {
- unsubscribeFocus = navigation.addListener('focus', () => document.addEventListener('paste', handlePaste, true));
- unsubscribeBlur = navigation.addListener('blur', () => document.removeEventListener('paste', handlePaste, true));
+ if (!isActive) {
+ return;
}
-
document.addEventListener('paste', handlePaste, true);
return () => {
- if (removeListenerOnScreenBlur) {
- unsubscribeFocus();
- unsubscribeBlur();
- }
document.removeEventListener('paste', handlePaste, true);
};
// eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps
- }, []);
+ }, [isActive]);
};
export default useHtmlPaste;
diff --git a/src/hooks/useHtmlPaste/types.ts b/src/hooks/useHtmlPaste/types.ts
index 0aaa12a49ac9..65778463f80e 100644
--- a/src/hooks/useHtmlPaste/types.ts
+++ b/src/hooks/useHtmlPaste/types.ts
@@ -4,7 +4,7 @@ import type {TextInput} from 'react-native';
type UseHtmlPaste = (
textInputRef: MutableRefObject<(HTMLTextAreaElement & TextInput) | TextInput | null>,
preHtmlPasteCallback?: (event: ClipboardEvent) => boolean,
- removeListenerOnScreenBlur?: boolean,
+ isActive?: boolean,
maxLength?: number, // Maximum length of the text input value after pasting
) => void;
diff --git a/src/hooks/useOnboardingFlow.ts b/src/hooks/useOnboardingFlow.ts
index 7aff640aed94..81796dae851d 100644
--- a/src/hooks/useOnboardingFlow.ts
+++ b/src/hooks/useOnboardingFlow.ts
@@ -1,11 +1,11 @@
import {useEffect} from 'react';
import {NativeModules} from 'react-native';
import {useOnyx} from 'react-native-onyx';
-import * as LoginUtils from '@libs/LoginUtils';
import Navigation from '@libs/Navigation/Navigation';
import {hasCompletedGuidedSetupFlowSelector, tryNewDotOnyxSelector} from '@libs/onboardingSelectors';
import Permissions from '@libs/Permissions';
import * as SearchQueryUtils from '@libs/SearchQueryUtils';
+import * as Session from '@userActions/Session';
import * as OnboardingFlow from '@userActions/Welcome/OnboardingFlow';
import ONYXKEYS from '@src/ONYXKEYS';
import ROUTES from '@src/ROUTES';
@@ -27,8 +27,7 @@ function useOnboardingFlowRouter() {
const [dismissedProductTraining, dismissedProductTrainingMetadata] = useOnyx(ONYXKEYS.NVP_DISMISSED_PRODUCT_TRAINING);
- const [session] = useOnyx(ONYXKEYS.SESSION);
- const isPrivateDomain = !!session?.email && !LoginUtils.isEmailPublicDomain(session?.email);
+ const isPrivateDomain = Session.isUserOnPrivateDomain();
const [isSingleNewDotEntry, isSingleNewDotEntryMetadata] = useOnyx(ONYXKEYS.IS_SINGLE_NEW_DOT_ENTRY);
const [allBetas, allBetasMetadata] = useOnyx(ONYXKEYS.BETAS);
diff --git a/src/languages/en.ts b/src/languages/en.ts
index 13aa4b17e358..904ab4cac128 100755
--- a/src/languages/en.ts
+++ b/src/languages/en.ts
@@ -61,6 +61,7 @@ import type {
DeleteConfirmationParams,
DidSplitAmountMessageParams,
EditActionParams,
+ EditDestinationSubtitleParams,
ElectronicFundsParams,
EnterMagicCodeParams,
ExportAgainModalDescriptionParams,
@@ -488,7 +489,6 @@ const translations = {
skip: 'Skip',
chatWithAccountManager: ({accountManagerDisplayName}: ChatWithAccountManagerParams) => `Need something specific? Chat with your account manager, ${accountManagerDisplayName}.`,
chatNow: 'Chat now',
- validate: 'Validate',
},
supportalNoAccess: {
title: 'Not so fast',
@@ -532,7 +532,7 @@ const translations = {
attachmentImageResized: 'This image has been resized for previewing. Download for full resolution.',
attachmentImageTooLarge: 'This image is too large to preview before uploading.',
tooManyFiles: ({fileLimit}: FileLimitParams) => `You can only upload up to ${fileLimit} files at a time.`,
- sizeExceededWithValue: ({maxUploadSizeInMB}: SizeExceededParams) => `Files exceeds ${maxUploadSizeInMB}MB. Please try again.`,
+ sizeExceededWithValue: ({maxUploadSizeInMB}: SizeExceededParams) => `Files exceeds ${maxUploadSizeInMB} MB. Please try again.`,
},
filePicker: {
fileError: 'File error',
@@ -1105,7 +1105,7 @@ const translations = {
viewPhoto: 'View photo',
imageUploadFailed: 'Image upload failed',
deleteWorkspaceError: 'Sorry, there was an unexpected problem deleting your workspace avatar',
- sizeExceeded: ({maxUploadSizeInMB}: SizeExceededParams) => `The selected image exceeds the maximum upload size of ${maxUploadSizeInMB}MB.`,
+ sizeExceeded: ({maxUploadSizeInMB}: SizeExceededParams) => `The selected image exceeds the maximum upload size of ${maxUploadSizeInMB} MB.`,
resolutionConstraints: ({minHeightInPx, minWidthInPx, maxHeightInPx, maxWidthInPx}: ResolutionConstraintsParams) =>
`Please upload an image larger than ${minHeightInPx}x${minWidthInPx} pixels and smaller than ${maxHeightInPx}x${maxWidthInPx} pixels.`,
notAllowedExtension: ({allowedExtensions}: NotAllowedExtensionParams) => `Profile picture must be one of the following types: ${allowedExtensions.join(', ')}.`,
@@ -1865,10 +1865,7 @@ const translations = {
toUnblock: ' to unblock your login.',
},
smsDeliveryFailurePage: {
- smsDeliveryFailureMessage: ({login}: OurEmailProviderParams) =>
- `We've been unable to deliver SMS messages to ${login}, so we've suspended it for 24 hours. Please try validating your number:`,
- validationFailed: 'Validation failed because it hasn’t been 24 hours since your last attempt.',
- validationSuccess: 'Your number has been validated! Click below to send a new magic sign-in code.',
+ smsDeliveryFailureMessage: ({login}: OurEmailProviderParams) => `We've been unable to deliver SMS messages to ${login}, so we've suspended it for 24 hours.`,
},
welcomeSignUpForm: {
join: 'Join',
@@ -2639,6 +2636,9 @@ const translations = {
existingRateError: ({rate}: CustomUnitRateParams) => `A rate with value ${rate} already exists.`,
},
importPerDiemRates: 'Import per diem rates',
+ editPerDiemRate: 'Edit per diem rate',
+ editDestinationSubtitle: ({destination}: EditDestinationSubtitleParams) => `Updating this destination will change it for all ${destination} per diem subrates.`,
+ editCurrencySubtitle: ({destination}: EditDestinationSubtitleParams) => `Updating this currency will change it for all ${destination} per diem subrates.`,
},
qbd: {
exportOutOfPocketExpensesDescription: 'Set how out-of-pocket expenses export to QuickBooks Desktop.',
diff --git a/src/languages/es.ts b/src/languages/es.ts
index 20202b2e176f..47163eaff018 100644
--- a/src/languages/es.ts
+++ b/src/languages/es.ts
@@ -60,6 +60,7 @@ import type {
DeleteConfirmationParams,
DidSplitAmountMessageParams,
EditActionParams,
+ EditDestinationSubtitleParams,
ElectronicFundsParams,
EnterMagicCodeParams,
ExportAgainModalDescriptionParams,
@@ -479,7 +480,6 @@ const translations = {
minuteAbbreviation: 'm',
chatWithAccountManager: ({accountManagerDisplayName}: ChatWithAccountManagerParams) => `¿Necesitas algo específico? Habla con tu gerente de cuenta, ${accountManagerDisplayName}.`,
chatNow: 'Chatear ahora',
- validate: 'Validar',
},
supportalNoAccess: {
title: 'No tan rápido',
@@ -527,7 +527,7 @@ const translations = {
attachmentImageResized: 'Se ha cambiado el tamaño de esta imagen para obtener una vista previa. Descargar para resolución completa.',
attachmentImageTooLarge: 'Esta imagen es demasiado grande para obtener una vista previa antes de subirla.',
tooManyFiles: ({fileLimit}: FileLimitParams) => `Solamente puedes suber ${fileLimit} archivos a la vez.`,
- sizeExceededWithValue: ({maxUploadSizeInMB}: SizeExceededParams) => `El archivo supera los ${maxUploadSizeInMB}MB. Por favor, vuelve a intentarlo.`,
+ sizeExceededWithValue: ({maxUploadSizeInMB}: SizeExceededParams) => `El archivo supera los ${maxUploadSizeInMB} MB. Por favor, vuelve a intentarlo.`,
},
filePicker: {
fileError: 'Error de archivo',
@@ -1103,7 +1103,7 @@ const translations = {
viewPhoto: 'Ver foto',
imageUploadFailed: 'Error al cargar la imagen',
deleteWorkspaceError: 'Lo sentimos, hubo un problema eliminando el avatar de tu espacio de trabajo',
- sizeExceeded: ({maxUploadSizeInMB}: SizeExceededParams) => `La imagen supera el tamaño máximo de ${maxUploadSizeInMB}MB.`,
+ sizeExceeded: ({maxUploadSizeInMB}: SizeExceededParams) => `La imagen supera el tamaño máximo de ${maxUploadSizeInMB} MB.`,
resolutionConstraints: ({minHeightInPx, minWidthInPx, maxHeightInPx, maxWidthInPx}: ResolutionConstraintsParams) =>
`Por favor, elige una imagen más grande que ${minHeightInPx}x${minWidthInPx} píxeles y más pequeña que ${maxHeightInPx}x${maxWidthInPx} píxeles.`,
notAllowedExtension: ({allowedExtensions}: NotAllowedExtensionParams) => `La foto de perfil debe ser de uno de los siguientes tipos: ${allowedExtensions.join(', ')}.`,
@@ -1870,10 +1870,7 @@ const translations = {
toUnblock: ' para desbloquear el inicio de sesión.',
},
smsDeliveryFailurePage: {
- smsDeliveryFailureMessage: ({login}: OurEmailProviderParams) =>
- `No hemos podido entregar mensajes SMS a ${login}, así que lo hemos suspendido durante 24 horas. Por favor, intenta validar tu número:`,
- validationFailed: 'La validación falló porque no han pasado 24 horas desde tu último intento.',
- validationSuccess: '¡Tu número ha sido validado! Haz clic abajo para enviar un nuevo código mágico de inicio de sesión.',
+ smsDeliveryFailureMessage: ({login}: OurEmailProviderParams) => `No hemos podido entregar mensajes SMS a ${login}, por lo que lo hemos suspendido durante 24 horas.`,
},
welcomeSignUpForm: {
join: 'Unirse',
@@ -2663,6 +2660,9 @@ const translations = {
existingRateError: ({rate}: CustomUnitRateParams) => `Ya existe una tasa con el valor ${rate}.`,
},
importPerDiemRates: 'Importar tasas de per diem',
+ editPerDiemRate: 'Editar la tasa de per diem',
+ editDestinationSubtitle: ({destination}: EditDestinationSubtitleParams) => `Actualizar este destino lo modificará para todas las subtasas per diem de ${destination}.`,
+ editCurrencySubtitle: ({destination}: EditDestinationSubtitleParams) => `Actualizar esta moneda la modificará para todas las subtasas per diem de ${destination}.`,
},
qbd: {
exportOutOfPocketExpensesDescription: 'Establezca cómo se exportan los gastos de bolsillo a QuickBooks Desktop.',
diff --git a/src/languages/params.ts b/src/languages/params.ts
index 59e1a061f7bf..f9ca26a3575a 100644
--- a/src/languages/params.ts
+++ b/src/languages/params.ts
@@ -586,6 +586,10 @@ type ChatWithAccountManagerParams = {
accountManagerDisplayName: string;
};
+type EditDestinationSubtitleParams = {
+ destination: string;
+};
+
type FlightLayoverParams = {
layover: string;
};
@@ -798,5 +802,6 @@ export type {
CompanyNameParams,
CustomUnitRateParams,
ChatWithAccountManagerParams,
+ EditDestinationSubtitleParams,
FlightLayoverParams,
};
diff --git a/src/libs/API/parameters/ResetSMSDeliveryFailureParams.ts b/src/libs/API/parameters/ResetSMSDeliveryFailureParams.ts
deleted file mode 100644
index f9a0bf41a218..000000000000
--- a/src/libs/API/parameters/ResetSMSDeliveryFailureParams.ts
+++ /dev/null
@@ -1,5 +0,0 @@
-type ResetSMSDeliveryFailureParams = {
- login: string;
-};
-
-export default ResetSMSDeliveryFailureParams;
diff --git a/src/libs/API/parameters/TransactionMergeParams.ts b/src/libs/API/parameters/TransactionMergeParams.ts
index 9e2516e2637f..ad718d37e6c8 100644
--- a/src/libs/API/parameters/TransactionMergeParams.ts
+++ b/src/libs/API/parameters/TransactionMergeParams.ts
@@ -1,5 +1,5 @@
type TransactionMergeParams = {
- transactionID: string;
+ transactionID: string | undefined;
transactionIDList: string[];
created: string;
merchant: string;
@@ -11,7 +11,7 @@ type TransactionMergeParams = {
reimbursable: boolean;
tag: string;
receiptID: number;
- reportID: string;
+ reportID: string | undefined;
};
export default TransactionMergeParams;
diff --git a/src/libs/API/parameters/UpdateWorkspaceCustomUnitParams.ts b/src/libs/API/parameters/UpdateWorkspaceCustomUnitParams.ts
new file mode 100644
index 000000000000..fa1fc3d8c911
--- /dev/null
+++ b/src/libs/API/parameters/UpdateWorkspaceCustomUnitParams.ts
@@ -0,0 +1,6 @@
+type UpdateWorkspaceCustomUnitParams = {
+ policyID: string;
+ customUnit: string;
+};
+
+export default UpdateWorkspaceCustomUnitParams;
diff --git a/src/libs/API/parameters/index.ts b/src/libs/API/parameters/index.ts
index ca68f3fcd4b7..f31e53de07e3 100644
--- a/src/libs/API/parameters/index.ts
+++ b/src/libs/API/parameters/index.ts
@@ -355,6 +355,6 @@ export type {default as TogglePlatformMuteParams} from './TogglePlatformMutePara
export type {default as JoinAccessiblePolicyParams} from './JoinAccessiblePolicyParams';
export type {default as ImportPerDiemRatesParams} from './ImportPerDiemRatesParams';
export type {default as ExportPerDiemCSVParams} from './ExportPerDiemCSVParams';
+export type {default as UpdateWorkspaceCustomUnitParams} from './UpdateWorkspaceCustomUnitParams';
export type {default as DismissProductTrainingParams} from './DismissProductTraining';
export type {default as OpenWorkspacePlanPageParams} from './OpenWorkspacePlanPage';
-export type {default as ResetSMSDeliveryFailureParams} from './ResetSMSDeliveryFailureParams';
diff --git a/src/libs/API/types.ts b/src/libs/API/types.ts
index f63be7c72f45..96ff46bf8fbb 100644
--- a/src/libs/API/types.ts
+++ b/src/libs/API/types.ts
@@ -439,9 +439,9 @@ const WRITE_COMMANDS = {
SELF_TOUR_VIEWED: 'SelfTourViewed',
UPDATE_INVOICE_COMPANY_NAME: 'UpdateInvoiceCompanyName',
UPDATE_INVOICE_COMPANY_WEBSITE: 'UpdateInvoiceCompanyWebsite',
+ UPDATE_WORKSPACE_CUSTOM_UNIT: 'UpdateWorkspaceCustomUnit',
VALIDATE_USER_AND_GET_ACCESSIBLE_POLICIES: 'ValidateUserAndGetAccessiblePolicies',
DISMISS_PRODUCT_TRAINING: 'DismissProductTraining',
- RESET_SMS_DELIVERY_FAILURE: 'ResetSMSDeliveryFailure',
} as const;
type WriteCommand = ValueOf;
@@ -767,7 +767,7 @@ type WriteCommandParameters = {
[WRITE_COMMANDS.UPDATE_SUBSCRIPTION_ADD_NEW_USERS_AUTOMATICALLY]: Parameters.UpdateSubscriptionAddNewUsersAutomaticallyParams;
[WRITE_COMMANDS.UPDATE_SUBSCRIPTION_SIZE]: Parameters.UpdateSubscriptionSizeParams;
[WRITE_COMMANDS.REQUEST_TAX_EXEMPTION]: null;
- [WRITE_COMMANDS.RESET_SMS_DELIVERY_FAILURE]: Parameters.ResetSMSDeliveryFailureParams;
+ [WRITE_COMMANDS.UPDATE_WORKSPACE_CUSTOM_UNIT]: Parameters.UpdateWorkspaceCustomUnitParams;
[WRITE_COMMANDS.DELETE_MONEY_REQUEST_ON_SEARCH]: Parameters.DeleteMoneyRequestOnSearchParams;
[WRITE_COMMANDS.HOLD_MONEY_REQUEST_ON_SEARCH]: Parameters.HoldMoneyRequestOnSearchParams;
diff --git a/src/libs/DistanceRequestUtils.ts b/src/libs/DistanceRequestUtils.ts
index fe40ea67f905..c41b33873a8a 100644
--- a/src/libs/DistanceRequestUtils.ts
+++ b/src/libs/DistanceRequestUtils.ts
@@ -292,17 +292,25 @@ function convertToDistanceInMeters(distance: number, unit: Unit): number {
function getCustomUnitRateID(reportID: string) {
const report = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${reportID}`];
const parentReport = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${report?.parentReportID}`];
- const policy = PolicyUtils.getPolicy(report?.policyID ?? parentReport?.policyID ?? '-1');
+ const policy = PolicyUtils.getPolicy(report?.policyID ?? parentReport?.policyID);
let customUnitRateID: string = CONST.CUSTOM_UNITS.FAKE_P2P_ID;
+ if (isEmptyObject(policy)) {
+ return customUnitRateID;
+ }
+
if (ReportUtils.isPolicyExpenseChat(report) || ReportUtils.isPolicyExpenseChat(parentReport)) {
- const distanceUnit = Object.values(policy?.customUnits ?? {}).find((unit) => unit.name === CONST.CUSTOM_UNITS.NAME_DISTANCE);
- const lastSelectedDistanceRateID = lastSelectedDistanceRates?.[policy?.id ?? '-1'] ?? '-1';
- const lastSelectedDistanceRate = distanceUnit?.rates[lastSelectedDistanceRateID] ?? {};
- if (lastSelectedDistanceRate.enabled && lastSelectedDistanceRateID) {
+ const distanceUnit = Object.values(policy.customUnits ?? {}).find((unit) => unit.name === CONST.CUSTOM_UNITS.NAME_DISTANCE);
+ const lastSelectedDistanceRateID = lastSelectedDistanceRates?.[policy.id];
+ const lastSelectedDistanceRate = lastSelectedDistanceRateID ? distanceUnit?.rates[lastSelectedDistanceRateID] : undefined;
+ if (lastSelectedDistanceRate?.enabled && lastSelectedDistanceRateID) {
customUnitRateID = lastSelectedDistanceRateID;
} else {
- customUnitRateID = getDefaultMileageRate(policy)?.customUnitRateID ?? '-1';
+ const defaultMileageRate = getDefaultMileageRate(policy);
+ if (!defaultMileageRate?.customUnitRateID) {
+ return customUnitRateID;
+ }
+ customUnitRateID = defaultMileageRate.customUnitRateID;
}
}
diff --git a/src/libs/GetPhysicalCardUtils.ts b/src/libs/GetPhysicalCardUtils.ts
index 8dc46204db3c..9ed192b09233 100644
--- a/src/libs/GetPhysicalCardUtils.ts
+++ b/src/libs/GetPhysicalCardUtils.ts
@@ -1,3 +1,4 @@
+import {Str} from 'expensify-common';
import type {OnyxEntry} from 'react-native-onyx';
import ROUTES from '@src/ROUTES';
import type {Route} from '@src/ROUTES';
@@ -6,16 +7,19 @@ import type {LoginList, PrivatePersonalDetails} from '@src/types/onyx';
import * as LoginUtils from './LoginUtils';
import Navigation from './Navigation/Navigation';
import * as PersonalDetailsUtils from './PersonalDetailsUtils';
+import * as PhoneNumberUtils from './PhoneNumber';
import * as UserUtils from './UserUtils';
function getCurrentRoute(domain: string, privatePersonalDetails: OnyxEntry): Route {
const {legalFirstName, legalLastName, phoneNumber} = privatePersonalDetails ?? {};
const address = PersonalDetailsUtils.getCurrentAddress(privatePersonalDetails);
+ const phoneNumberWithCountryCode = LoginUtils.appendCountryCode(phoneNumber ?? '');
+ const parsedPhoneNumber = PhoneNumberUtils.parsePhoneNumber(phoneNumberWithCountryCode);
if (!legalFirstName && !legalLastName) {
return ROUTES.SETTINGS_WALLET_CARD_GET_PHYSICAL_NAME.getRoute(domain);
}
- if (!phoneNumber || !LoginUtils.validateNumber(phoneNumber)) {
+ if (!phoneNumber || !parsedPhoneNumber.possible || !Str.isValidE164Phone(phoneNumberWithCountryCode.slice(0))) {
return ROUTES.SETTINGS_WALLET_CARD_GET_PHYSICAL_PHONE.getRoute(domain);
}
if (!(address?.street && address?.city && address?.state && address?.country && address?.zip)) {
diff --git a/src/libs/Middleware/SaveResponseInOnyx.ts b/src/libs/Middleware/SaveResponseInOnyx.ts
index 85f95c146dac..12c1931b0199 100644
--- a/src/libs/Middleware/SaveResponseInOnyx.ts
+++ b/src/libs/Middleware/SaveResponseInOnyx.ts
@@ -10,7 +10,6 @@ const requestsToIgnoreLastUpdateID: string[] = [
SIDE_EFFECT_REQUEST_COMMANDS.RECONNECT_APP,
WRITE_COMMANDS.CLOSE_ACCOUNT,
WRITE_COMMANDS.DELETE_MONEY_REQUEST,
- WRITE_COMMANDS.SUBMIT_REPORT,
SIDE_EFFECT_REQUEST_COMMANDS.GET_MISSING_ONYX_MESSAGES,
];
diff --git a/src/libs/MoneyRequestUtils.ts b/src/libs/MoneyRequestUtils.ts
index 206bb8509af6..d76c9325cc0e 100644
--- a/src/libs/MoneyRequestUtils.ts
+++ b/src/libs/MoneyRequestUtils.ts
@@ -32,19 +32,23 @@ function stripDecimalsFromAmount(amount: string): string {
* Adds a leading zero to the amount if user entered just the decimal separator
*
* @param amount - Changed amount from user input
+ * @param shouldAllowNegative - Should allow negative numbers
*/
-function addLeadingZero(amount: string): string {
+function addLeadingZero(amount: string, shouldAllowNegative = false): string {
+ if (shouldAllowNegative && amount.startsWith('-.')) {
+ return `-0${amount}`;
+ }
return amount.startsWith('.') ? `0${amount}` : amount;
}
/**
* Check if amount is a decimal up to 3 digits
*/
-function validateAmount(amount: string, decimals: number, amountMaxLength: number = CONST.IOU.AMOUNT_MAX_LENGTH): boolean {
+function validateAmount(amount: string, decimals: number, amountMaxLength: number = CONST.IOU.AMOUNT_MAX_LENGTH, shouldAllowNegative = false): boolean {
const regexString =
decimals === 0
- ? `^\\d{1,${amountMaxLength}}$` // Don't allow decimal point if decimals === 0
- : `^\\d{1,${amountMaxLength}}(\\.\\d{0,${decimals}})?$`; // Allow the decimal point and the desired number of digits after the point
+ ? `^${shouldAllowNegative ? '-?' : ''}\\d{1,${amountMaxLength}}$` // Don't allow decimal point if decimals === 0
+ : `^${shouldAllowNegative ? '-?' : ''}\\d{1,${amountMaxLength}}(\\.\\d{0,${decimals}})?$`; // Allow the decimal point and the desired number of digits after the point
const decimalNumberRegex = new RegExp(regexString, 'i');
return amount === '' || decimalNumberRegex.test(amount);
}
diff --git a/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx b/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx
index 82e762cf033d..9622aca72c39 100644
--- a/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx
+++ b/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx
@@ -576,6 +576,11 @@ const SettingsModalStackNavigator = createModalStackNavigator require('../../../../pages/workspace/perDiem/ImportPerDiemPage').default,
[SCREENS.WORKSPACE.PER_DIEM_IMPORTED]: () => require('../../../../pages/workspace/perDiem/ImportedPerDiemPage').default,
[SCREENS.WORKSPACE.PER_DIEM_SETTINGS]: () => require('../../../../pages/workspace/perDiem/WorkspacePerDiemSettingsPage').default,
+ [SCREENS.WORKSPACE.PER_DIEM_DETAILS]: () => require('../../../../pages/workspace/perDiem/WorkspacePerDiemDetailsPage').default,
+ [SCREENS.WORKSPACE.PER_DIEM_EDIT_DESTINATION]: () => require('../../../../pages/workspace/perDiem/EditPerDiemDestinationPage').default,
+ [SCREENS.WORKSPACE.PER_DIEM_EDIT_SUBRATE]: () => require('../../../../pages/workspace/perDiem/EditPerDiemSubratePage').default,
+ [SCREENS.WORKSPACE.PER_DIEM_EDIT_AMOUNT]: () => require('../../../../pages/workspace/perDiem/EditPerDiemAmountPage').default,
+ [SCREENS.WORKSPACE.PER_DIEM_EDIT_CURRENCY]: () => require('../../../../pages/workspace/perDiem/EditPerDiemCurrencyPage').default,
});
const EnablePaymentsStackNavigator = createModalStackNavigator({
diff --git a/src/libs/Navigation/NavigationRoot.tsx b/src/libs/Navigation/NavigationRoot.tsx
index d6e021d014b3..b19635a77fdb 100644
--- a/src/libs/Navigation/NavigationRoot.tsx
+++ b/src/libs/Navigation/NavigationRoot.tsx
@@ -12,10 +12,10 @@ import useThemePreference from '@hooks/useThemePreference';
import Firebase from '@libs/Firebase';
import {FSPage} from '@libs/Fullstory';
import Log from '@libs/Log';
-import * as LoginUtils from '@libs/LoginUtils';
import {hasCompletedGuidedSetupFlowSelector} from '@libs/onboardingSelectors';
import {getPathFromURL} from '@libs/Url';
import {updateLastVisitedPath} from '@userActions/App';
+import * as Session from '@userActions/Session';
import {updateOnboardingLastVisitedPath} from '@userActions/Welcome';
import {getOnboardingInitialPath} from '@userActions/Welcome/OnboardingFlow';
import CONST from '@src/CONST';
@@ -93,8 +93,7 @@ function NavigationRoot({authenticated, lastVisitedPath, initialUrl, onReady, sh
const {shouldUseNarrowLayout} = useResponsiveLayout();
const {setActiveWorkspaceID} = useActiveWorkspace();
const [user] = useOnyx(ONYXKEYS.USER);
- const [session] = useOnyx(ONYXKEYS.SESSION);
- const isPrivateDomain = !!session?.email && !LoginUtils.isEmailPublicDomain(session?.email);
+ const isPrivateDomain = Session.isUserOnPrivateDomain();
const [isOnboardingCompleted = true] = useOnyx(ONYXKEYS.NVP_ONBOARDING, {
selector: hasCompletedGuidedSetupFlowSelector,
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 4365c5e65e25..bd4497fbcc58 100755
--- a/src/libs/Navigation/linkingConfig/FULL_SCREEN_TO_RHP_MAPPING.ts
+++ b/src/libs/Navigation/linkingConfig/FULL_SCREEN_TO_RHP_MAPPING.ts
@@ -246,7 +246,16 @@ const FULL_SCREEN_TO_RHP_MAPPING: Partial> = {
SCREENS.WORKSPACE.RULES_MAX_EXPENSE_AGE,
SCREENS.WORKSPACE.RULES_BILLABLE_DEFAULT,
],
- [SCREENS.WORKSPACE.PER_DIEM]: [SCREENS.WORKSPACE.PER_DIEM_IMPORT, SCREENS.WORKSPACE.PER_DIEM_IMPORTED, SCREENS.WORKSPACE.PER_DIEM_SETTINGS],
+ [SCREENS.WORKSPACE.PER_DIEM]: [
+ SCREENS.WORKSPACE.PER_DIEM_IMPORT,
+ SCREENS.WORKSPACE.PER_DIEM_IMPORTED,
+ SCREENS.WORKSPACE.PER_DIEM_SETTINGS,
+ SCREENS.WORKSPACE.PER_DIEM_DETAILS,
+ SCREENS.WORKSPACE.PER_DIEM_EDIT_DESTINATION,
+ SCREENS.WORKSPACE.PER_DIEM_EDIT_SUBRATE,
+ SCREENS.WORKSPACE.PER_DIEM_EDIT_AMOUNT,
+ SCREENS.WORKSPACE.PER_DIEM_EDIT_CURRENCY,
+ ],
};
export default FULL_SCREEN_TO_RHP_MAPPING;
diff --git a/src/libs/Navigation/linkingConfig/config.ts b/src/libs/Navigation/linkingConfig/config.ts
index 30b64a79dca2..04ed0261a225 100644
--- a/src/libs/Navigation/linkingConfig/config.ts
+++ b/src/libs/Navigation/linkingConfig/config.ts
@@ -974,6 +974,21 @@ const config: LinkingOptions['config'] = {
[SCREENS.WORKSPACE.PER_DIEM_SETTINGS]: {
path: ROUTES.WORKSPACE_PER_DIEM_SETTINGS.route,
},
+ [SCREENS.WORKSPACE.PER_DIEM_DETAILS]: {
+ path: ROUTES.WORKSPACE_PER_DIEM_DETAILS.route,
+ },
+ [SCREENS.WORKSPACE.PER_DIEM_EDIT_DESTINATION]: {
+ path: ROUTES.WORKSPACE_PER_DIEM_EDIT_DESTINATION.route,
+ },
+ [SCREENS.WORKSPACE.PER_DIEM_EDIT_SUBRATE]: {
+ path: ROUTES.WORKSPACE_PER_DIEM_EDIT_SUBRATE.route,
+ },
+ [SCREENS.WORKSPACE.PER_DIEM_EDIT_AMOUNT]: {
+ path: ROUTES.WORKSPACE_PER_DIEM_EDIT_AMOUNT.route,
+ },
+ [SCREENS.WORKSPACE.PER_DIEM_EDIT_CURRENCY]: {
+ path: ROUTES.WORKSPACE_PER_DIEM_EDIT_CURRENCY.route,
+ },
},
},
[SCREENS.RIGHT_MODAL.PRIVATE_NOTES]: {
diff --git a/src/libs/Navigation/types.ts b/src/libs/Navigation/types.ts
index 27671fac401f..71f11113e84c 100644
--- a/src/libs/Navigation/types.ts
+++ b/src/libs/Navigation/types.ts
@@ -908,6 +908,31 @@ type SettingsNavigatorParamList = {
[SCREENS.WORKSPACE.PER_DIEM_SETTINGS]: {
policyID: string;
};
+ [SCREENS.WORKSPACE.PER_DIEM_DETAILS]: {
+ policyID: string;
+ rateID: string;
+ subRateID: string;
+ };
+ [SCREENS.WORKSPACE.PER_DIEM_EDIT_DESTINATION]: {
+ policyID: string;
+ rateID: string;
+ subRateID: string;
+ };
+ [SCREENS.WORKSPACE.PER_DIEM_EDIT_SUBRATE]: {
+ policyID: string;
+ rateID: string;
+ subRateID: string;
+ };
+ [SCREENS.WORKSPACE.PER_DIEM_EDIT_AMOUNT]: {
+ policyID: string;
+ rateID: string;
+ subRateID: string;
+ };
+ [SCREENS.WORKSPACE.PER_DIEM_EDIT_CURRENCY]: {
+ policyID: string;
+ rateID: string;
+ subRateID: string;
+ };
} & ReimbursementAccountNavigatorParamList;
type NewChatNavigatorParamList = {
diff --git a/src/libs/PolicyUtils.ts b/src/libs/PolicyUtils.ts
index 458adfd311d4..4982e8660dec 100644
--- a/src/libs/PolicyUtils.ts
+++ b/src/libs/PolicyUtils.ts
@@ -182,7 +182,7 @@ function getRateDisplayValue(value: number, toLocaleDigit: (arg: string) => stri
return numValue.toString().replace('.', toLocaleDigit('.')).substring(0, value.toString().length);
}
-function getUnitRateValue(toLocaleDigit: (arg: string) => string, customUnitRate?: Rate, withDecimals?: boolean) {
+function getUnitRateValue(toLocaleDigit: (arg: string) => string, customUnitRate?: Partial, withDecimals?: boolean) {
return getRateDisplayValue((customUnitRate?.rate ?? 0) / CONST.POLICY.CUSTOM_UNIT_RATE_BASE_OFFSET, toLocaleDigit, withDecimals);
}
diff --git a/src/libs/TransactionUtils/index.ts b/src/libs/TransactionUtils/index.ts
index 2304132e79f1..6643cd721d45 100644
--- a/src/libs/TransactionUtils/index.ts
+++ b/src/libs/TransactionUtils/index.ts
@@ -9,7 +9,8 @@ import type {ValueOf} from 'type-fest';
import {getPolicyCategoriesData} from '@libs/actions/Policy/Category';
import {getPolicyTagsData} from '@libs/actions/Policy/Tag';
import type {TransactionMergeParams} from '@libs/API/parameters';
-import {getCurrencyDecimals} from '@libs/CurrencyUtils';
+import {getCategoryDefaultTaxRate} from '@libs/CategoryUtils';
+import {convertToBackendAmount, getCurrencyDecimals} from '@libs/CurrencyUtils';
import DateUtils from '@libs/DateUtils';
import DistanceRequestUtils from '@libs/DistanceRequestUtils';
import {toLocaleDigit} from '@libs/LocaleDigitUtils';
@@ -77,7 +78,7 @@ Onyx.connect({
key: ONYXKEYS.SESSION,
callback: (val) => {
currentUserEmail = val?.email ?? '';
- currentUserAccountID = val?.accountID ?? -1;
+ currentUserAccountID = val?.accountID ?? CONST.DEFAULT_NUMBER_ID;
},
});
@@ -579,7 +580,7 @@ function getCategory(transaction: OnyxInputOrEntry): string {
* Return the cardID from the transaction.
*/
function getCardID(transaction: Transaction): number {
- return transaction?.cardID ?? -1;
+ return transaction?.cardID ?? CONST.DEFAULT_NUMBER_ID;
}
/**
@@ -964,7 +965,7 @@ function getDefaultTaxCode(policy: OnyxEntry, transaction: OnyxEntry, transaction: OnyxEntry taxRate.code === (transaction?.taxCode ?? defaultTaxCode))?.modifiedName;
}
-function getTransaction(transactionID: string): OnyxEntry {
+function getTransaction(transactionID: string | undefined): OnyxEntry {
return allTransactions?.[`${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`];
}
@@ -1058,7 +1059,15 @@ function removeSettledAndApprovedTransactions(transactionIDs: string[]) {
* 6. It returns the 'keep' and 'change' objects.
*/
-function compareDuplicateTransactionFields(reviewingTransactionID: string, reportID: string, selectedTransactionID?: string): {keep: Partial; change: FieldsToChange} {
+function compareDuplicateTransactionFields(
+ reviewingTransactionID: string | undefined,
+ reportID: string | undefined,
+ selectedTransactionID?: string,
+): {keep: Partial; change: FieldsToChange} {
+ if (!reviewingTransactionID || !reportID) {
+ return {change: {}, keep: {}};
+ }
+
const transactionViolations = allTransactionViolations?.[`${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${reviewingTransactionID}`];
const duplicates = transactionViolations?.find((violation) => violation.name === CONST.VIOLATIONS.DUPLICATED_TRANSACTION)?.data?.duplicates ?? [];
const transactions = removeSettledAndApprovedTransactions([reviewingTransactionID, ...duplicates]).map((item) => getTransaction(item));
@@ -1159,7 +1168,7 @@ function compareDuplicateTransactionFields(reviewingTransactionID: string, repor
}
} else if (fieldName === 'category') {
const differentValues = getDifferentValues(transactions, keys);
- const policyCategories = getPolicyCategoriesData(report?.policyID ?? '-1');
+ const policyCategories = report?.policyID ? getPolicyCategoriesData(report.policyID) : {};
const availableCategories = Object.values(policyCategories)
.filter((category) => differentValues.includes(category.name) && category.enabled && category.pendingAction !== CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE)
.map((e) => e.name);
@@ -1170,7 +1179,7 @@ function compareDuplicateTransactionFields(reviewingTransactionID: string, repor
keep[fieldName] = firstTransaction?.[keys[0]] ?? firstTransaction?.[keys[1]];
}
} else if (fieldName === 'tag') {
- const policyTags = getPolicyTagsData(report?.policyID ?? '-1');
+ const policyTags = report?.policyID ? getPolicyTagsData(report?.policyID) : {};
const isMultiLevelTags = PolicyUtils.isMultiLevelTags(policyTags);
if (isMultiLevelTags) {
if (areAllFieldsEqualForKey || !policy?.areTagsEnabled) {
@@ -1201,10 +1210,10 @@ function compareDuplicateTransactionFields(reviewingTransactionID: string, repor
return {keep, change};
}
-function getTransactionID(threadReportID: string): string {
+function getTransactionID(threadReportID: string): string | undefined {
const report = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${threadReportID}`];
- const parentReportAction = ReportActionsUtils.getReportAction(report?.parentReportID ?? '', report?.parentReportActionID ?? '');
- const IOUTransactionID = ReportActionsUtils.isMoneyRequestAction(parentReportAction) ? ReportActionsUtils.getOriginalMessage(parentReportAction)?.IOUTransactionID ?? '-1' : '-1';
+ const parentReportAction = ReportUtils.isThread(report) ? ReportActionsUtils.getReportAction(report.parentReportID, report.parentReportActionID) : undefined;
+ const IOUTransactionID = ReportActionsUtils.isMoneyRequestAction(parentReportAction) ? ReportActionsUtils.getOriginalMessage(parentReportAction)?.IOUTransactionID : undefined;
return IOUTransactionID;
}
@@ -1225,11 +1234,11 @@ function buildNewTransactionAfterReviewingDuplicates(reviewDuplicateTransaction:
function buildTransactionsMergeParams(reviewDuplicates: OnyxEntry, originalTransaction: Partial): TransactionMergeParams {
return {
amount: -getAmount(originalTransaction as OnyxEntry, false),
- reportID: originalTransaction?.reportID ?? '',
- receiptID: originalTransaction?.receipt?.receiptID ?? 0,
+ reportID: originalTransaction?.reportID,
+ receiptID: originalTransaction?.receipt?.receiptID ?? CONST.DEFAULT_NUMBER_ID,
currency: getCurrency(originalTransaction as OnyxEntry),
created: getFormattedCreated(originalTransaction as OnyxEntry),
- transactionID: reviewDuplicates?.transactionID ?? '',
+ transactionID: reviewDuplicates?.transactionID,
transactionIDList: removeSettledAndApprovedTransactions(reviewDuplicates?.duplicates ?? []),
billable: reviewDuplicates?.billable ?? false,
reimbursable: reviewDuplicates?.reimbursable ?? false,
@@ -1240,6 +1249,23 @@ function buildTransactionsMergeParams(reviewDuplicates: OnyxEntry, policy: OnyxEntry) {
+ const taxRules = policy?.rules?.expenseRules?.filter((rule) => rule.tax);
+ if (!taxRules || taxRules?.length === 0) {
+ return {categoryTaxCode: undefined, categoryTaxAmount: undefined};
+ }
+
+ const categoryTaxCode = getCategoryDefaultTaxRate(taxRules, category, policy?.taxRates?.defaultExternalID);
+ const categoryTaxPercentage = getTaxValue(policy, transaction, categoryTaxCode ?? '');
+ let categoryTaxAmount;
+
+ if (categoryTaxPercentage) {
+ categoryTaxAmount = convertToBackendAmount(calculateTaxAmount(categoryTaxPercentage, getAmount(transaction), getCurrency(transaction)));
+ }
+
+ return {categoryTaxCode, categoryTaxAmount};
+}
+
/**
* Return the sorted list transactions of an iou report
*/
@@ -1342,6 +1368,7 @@ export {
shouldShowAttendees,
getAllSortedTransactions,
getFormattedPostedDate,
+ getCategoryTaxCodeAndAmount,
};
export type {TransactionChanges};
diff --git a/src/libs/actions/IOU.ts b/src/libs/actions/IOU.ts
index 5f8b83d4b5c2..284abf31a896 100644
--- a/src/libs/actions/IOU.ts
+++ b/src/libs/actions/IOU.ts
@@ -590,8 +590,19 @@ function setMoneyRequestPendingFields(transactionID: string, pendingFields: Onyx
Onyx.merge(`${ONYXKEYS.COLLECTION.TRANSACTION_DRAFT}${transactionID}`, {pendingFields});
}
-function setMoneyRequestCategory(transactionID: string, category: string) {
+function setMoneyRequestCategory(transactionID: string, category: string, policyID?: string) {
Onyx.merge(`${ONYXKEYS.COLLECTION.TRANSACTION_DRAFT}${transactionID}`, {category});
+ if (!policyID) {
+ setMoneyRequestTaxRate(transactionID, '');
+ setMoneyRequestTaxAmount(transactionID, null);
+ return;
+ }
+ const transaction = allTransactionDrafts[`${ONYXKEYS.COLLECTION.TRANSACTION_DRAFT}${transactionID}`];
+ const {categoryTaxCode, categoryTaxAmount} = TransactionUtils.getCategoryTaxCodeAndAmount(category, transaction, PolicyUtils.getPolicy(policyID));
+ if (categoryTaxCode && categoryTaxAmount !== undefined) {
+ setMoneyRequestTaxRate(transactionID, categoryTaxCode);
+ setMoneyRequestTaxAmount(transactionID, categoryTaxAmount);
+ }
}
function setMoneyRequestTag(transactionID: string, tag: string) {
@@ -3432,9 +3443,17 @@ function updateMoneyRequestCategory(
policyTagList: OnyxEntry,
policyCategories: OnyxEntry,
) {
+ const transaction = allTransactions[`${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`];
+ const {categoryTaxCode, categoryTaxAmount} = TransactionUtils.getCategoryTaxCodeAndAmount(category, transaction, policy);
const transactionChanges: TransactionChanges = {
category,
+ ...(categoryTaxCode &&
+ categoryTaxAmount !== undefined && {
+ taxCode: categoryTaxCode,
+ taxAmount: categoryTaxAmount,
+ }),
};
+
const {params, onyxData} = getUpdateMoneyRequestParams(transactionID, transactionThreadReportID, transactionChanges, policy, policyTagList, policyCategories);
API.write(WRITE_COMMANDS.UPDATE_MONEY_REQUEST_CATEGORY, params, onyxData);
}
@@ -5359,17 +5378,26 @@ function completeSplitBill(chatReportID: string, reportAction: OnyxTypes.ReportA
Report.notifyNewAction(chatReportID, sessionAccountID);
}
-function setDraftSplitTransaction(transactionID: string, transactionChanges: TransactionChanges = {}) {
+function setDraftSplitTransaction(transactionID: string, transactionChanges: TransactionChanges = {}, policy?: OnyxEntry) {
+ const newTransactionChanges = {...transactionChanges};
let draftSplitTransaction = allDraftSplitTransactions[`${ONYXKEYS.COLLECTION.SPLIT_TRANSACTION_DRAFT}${transactionID}`];
if (!draftSplitTransaction) {
draftSplitTransaction = allTransactions[`${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`];
}
+ if (transactionChanges.category) {
+ const {categoryTaxCode, categoryTaxAmount} = TransactionUtils.getCategoryTaxCodeAndAmount(transactionChanges.category, draftSplitTransaction, policy);
+ if (categoryTaxCode && categoryTaxAmount !== undefined) {
+ newTransactionChanges.taxCode = categoryTaxCode;
+ newTransactionChanges.taxAmount = categoryTaxAmount;
+ }
+ }
+
const updatedTransaction = draftSplitTransaction
? TransactionUtils.getUpdatedTransaction({
transaction: draftSplitTransaction,
- transactionChanges,
+ transactionChanges: newTransactionChanges,
isFromExpenseReport: false,
shouldUpdateReceiptState: false,
})
@@ -8577,7 +8605,7 @@ function mergeDuplicates(params: TransactionMergeParams) {
},
};
- const iouActionsToDelete = getIOUActionForTransactions(params.transactionIDList, params.reportID);
+ const iouActionsToDelete = params.reportID ? getIOUActionForTransactions(params.transactionIDList, params.reportID) : [];
const deletedTime = DateUtils.getDBTime();
const expenseReportActionsOptimisticData: OnyxUpdate = {
@@ -8644,6 +8672,10 @@ function updateLastLocationPermissionPrompt() {
/** Instead of merging the duplicates, it updates the transaction we want to keep and puts the others on hold without deleting them */
function resolveDuplicates(params: TransactionMergeParams) {
+ if (!params.transactionID) {
+ return;
+ }
+
const originalSelectedTransaction = allTransactions[`${ONYXKEYS.COLLECTION.TRANSACTION}${params.transactionID}`];
const optimisticTransactionData: OnyxUpdate = {
@@ -8691,7 +8723,7 @@ function resolveDuplicates(params: TransactionMergeParams) {
};
});
- const iouActionList = getIOUActionForTransactions(params.transactionIDList, params.reportID);
+ const iouActionList = params.reportID ? getIOUActionForTransactions(params.transactionIDList, params.reportID) : [];
const transactionThreadReportIDList = iouActionList.map((action) => action?.childReportID);
const orderedTransactionIDList = iouActionList.map((action) => {
const message = ReportActionsUtils.getOriginalMessage(action);
@@ -8743,7 +8775,7 @@ function resolveDuplicates(params: TransactionMergeParams) {
});
});
- const transactionThreadReportID = getIOUActionForTransactions([params.transactionID], params.reportID).at(0)?.childReportID;
+ const transactionThreadReportID = params.reportID ? getIOUActionForTransactions([params.transactionID], params.reportID).at(0)?.childReportID : undefined;
const optimisticReportAction = ReportUtils.buildOptimisticDismissedViolationReportAction({
reason: 'manual',
violationName: CONST.VIOLATIONS.DUPLICATED_TRANSACTION,
@@ -8774,6 +8806,7 @@ function resolveDuplicates(params: TransactionMergeParams) {
const parameters: ResolveDuplicatesParams = {
...otherParams,
+ transactionID: params.transactionID,
reportActionIDList,
transactionIDList: orderedTransactionIDList,
dismissedViolationReportActionID: optimisticReportAction.reportActionID,
diff --git a/src/libs/actions/Policy/DistanceRate.ts b/src/libs/actions/Policy/DistanceRate.ts
index 786e22ef91d9..2a4cf0cde1a7 100644
--- a/src/libs/actions/Policy/DistanceRate.ts
+++ b/src/libs/actions/Policy/DistanceRate.ts
@@ -129,34 +129,36 @@ function enablePolicyDistanceRates(policyID: string, enabled: boolean) {
if (!enabled) {
const policy = allPolicies?.[`${ONYXKEYS.COLLECTION.POLICY}${policyID}`];
const customUnit = getDistanceRateCustomUnit(policy);
- const customUnitID = customUnit?.customUnitID ?? '';
+ if (customUnit) {
+ const customUnitID = customUnit.customUnitID;
- const rateEntries = Object.entries(customUnit?.rates ?? {});
- // find the rate to be enabled after disabling the distance rate feature
- const rateEntryToBeEnabled = rateEntries.at(0);
+ const rateEntries = Object.entries(customUnit.rates ?? {});
+ // find the rate to be enabled after disabling the distance rate feature
+ const rateEntryToBeEnabled = rateEntries.at(0);
- onyxData.optimisticData?.push({
- onyxMethod: Onyx.METHOD.MERGE,
- key: `${ONYXKEYS.COLLECTION.POLICY}${policyID}`,
- value: {
- customUnits: {
- [customUnitID]: {
- rates: Object.fromEntries(
- rateEntries.map((rateEntry) => {
- const [rateID, rate] = rateEntry;
- return [
- rateID,
- {
- ...rate,
- enabled: rateID === rateEntryToBeEnabled?.at(0),
- },
- ];
- }),
- ),
+ onyxData.optimisticData?.push({
+ onyxMethod: Onyx.METHOD.MERGE,
+ key: `${ONYXKEYS.COLLECTION.POLICY}${policyID}`,
+ value: {
+ customUnits: {
+ [customUnitID]: {
+ rates: Object.fromEntries(
+ rateEntries.map((rateEntry) => {
+ const [rateID, rate] = rateEntry;
+ return [
+ rateID,
+ {
+ ...rate,
+ enabled: rateID === rateEntryToBeEnabled?.at(0),
+ },
+ ];
+ }),
+ ),
+ },
},
},
- },
- });
+ });
+ }
}
const parameters: EnablePolicyDistanceRatesParams = {policyID, enabled};
@@ -177,7 +179,7 @@ function createPolicyDistanceRate(policyID: string, customUnitID: string, custom
customUnits: {
[customUnitID]: {
rates: {
- [customUnitRate.customUnitRateID ?? '']: {
+ [customUnitRate.customUnitRateID]: {
...customUnitRate,
pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD,
},
@@ -196,7 +198,7 @@ function createPolicyDistanceRate(policyID: string, customUnitID: string, custom
customUnits: {
[customUnitID]: {
rates: {
- [customUnitRate.customUnitRateID ?? '']: {
+ [customUnitRate.customUnitRateID]: {
pendingAction: null,
},
},
@@ -214,7 +216,7 @@ function createPolicyDistanceRate(policyID: string, customUnitID: string, custom
customUnits: {
[customUnitID]: {
rates: {
- [customUnitRate.customUnitRateID ?? '']: {
+ [customUnitRate.customUnitRateID]: {
errors: ErrorUtils.getMicroSecondOnyxErrorWithTranslationKey('common.genericErrorMessage'),
},
},
@@ -339,9 +341,9 @@ function setPolicyDistanceRatesUnit(policyID: string, currentCustomUnit: CustomU
function updatePolicyDistanceRateValue(policyID: string, customUnit: CustomUnit, customUnitRates: Rate[]) {
const currentRates = customUnit.rates;
- const optimisticRates: Record = {};
- const successRates: Record = {};
- const failureRates: Record = {};
+ const optimisticRates: Record> = {};
+ const successRates: Record> = {};
+ const failureRates: Record> = {};
const rateIDs = customUnitRates.map((rate) => rate.customUnitRateID);
for (const rateID of Object.keys(customUnit.rates)) {
@@ -410,9 +412,9 @@ function updatePolicyDistanceRateValue(policyID: string, customUnit: CustomUnit,
function setPolicyDistanceRatesEnabled(policyID: string, customUnit: CustomUnit, customUnitRates: Rate[]) {
const currentRates = customUnit.rates;
- const optimisticRates: Record = {};
- const successRates: Record = {};
- const failureRates: Record = {};
+ const optimisticRates: Record> = {};
+ const successRates: Record> = {};
+ const failureRates: Record> = {};
const rateIDs = customUnitRates.map((rate) => rate.customUnitRateID);
for (const rateID of Object.keys(currentRates)) {
@@ -559,9 +561,9 @@ function deletePolicyDistanceRates(policyID: string, customUnit: CustomUnit, rat
function updateDistanceTaxClaimableValue(policyID: string, customUnit: CustomUnit, customUnitRates: Rate[]) {
const currentRates = customUnit.rates;
- const optimisticRates: Record = {};
- const successRates: Record = {};
- const failureRates: Record = {};
+ const optimisticRates: Record> = {};
+ const successRates: Record> = {};
+ const failureRates: Record> = {};
const rateIDs = customUnitRates.map((rate) => rate.customUnitRateID);
for (const rateID of Object.keys(customUnit.rates)) {
@@ -630,9 +632,9 @@ function updateDistanceTaxClaimableValue(policyID: string, customUnit: CustomUni
function updateDistanceTaxRate(policyID: string, customUnit: CustomUnit, customUnitRates: Rate[]) {
const currentRates = customUnit.rates;
- const optimisticRates: Record = {};
- const successRates: Record = {};
- const failureRates: Record = {};
+ const optimisticRates: Record> = {};
+ const successRates: Record> = {};
+ const failureRates: Record> = {};
const rateIDs = customUnitRates.map((rate) => rate.customUnitRateID);
for (const rateID of Object.keys(customUnit.rates)) {
diff --git a/src/libs/actions/Policy/PerDiem.ts b/src/libs/actions/Policy/PerDiem.ts
index 81898dfb34e0..323045e49821 100644
--- a/src/libs/actions/Policy/PerDiem.ts
+++ b/src/libs/actions/Policy/PerDiem.ts
@@ -1,3 +1,4 @@
+import lodashDeepClone from 'lodash/cloneDeep';
import type {NullishDeep, OnyxCollection} from 'react-native-onyx';
import Onyx from 'react-native-onyx';
import * as API from '@libs/API';
@@ -13,9 +14,10 @@ import * as ReportUtils from '@libs/ReportUtils';
import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
import type {Policy, Report} from '@src/types/onyx';
-import type {ErrorFields} from '@src/types/onyx/OnyxCommon';
-import type {Rate} from '@src/types/onyx/Policy';
+import type {ErrorFields, PendingAction} from '@src/types/onyx/OnyxCommon';
+import type {CustomUnit, Rate} from '@src/types/onyx/Policy';
import type {OnyxData} from '@src/types/onyx/Request';
+import {isEmptyObject} from '@src/types/utils/EmptyObject';
const allPolicies: OnyxCollection = {};
Onyx.connect({
@@ -50,6 +52,16 @@ Onyx.connect({
},
});
+type SubRateData = {
+ pendingAction?: PendingAction;
+ destination: string;
+ subRateName: string;
+ rate: number;
+ currency: string;
+ rateID: string;
+ subRateID: string;
+};
+
/**
* Returns a client generated 13 character hexadecimal value for a custom unit ID
*/
@@ -193,4 +205,208 @@ function clearPolicyPerDiemRatesErrorFields(policyID: string, customUnitID: stri
});
}
-export {generateCustomUnitID, enablePerDiem, openPolicyPerDiemPage, importPerDiemRates, downloadPerDiemCSV, clearPolicyPerDiemRatesErrorFields};
+type DeletePerDiemCustomUnitOnyxType = Omit & {
+ rates: Record | null>;
+};
+
+function prepareNewCustomUnit(customUnit: CustomUnit, subRatesToBeDeleted: SubRateData[]) {
+ const mappedDeletedSubRatesToRate = subRatesToBeDeleted.reduce((acc, subRate) => {
+ if (subRate.rateID in acc) {
+ acc[subRate.rateID].push(subRate);
+ } else {
+ acc[subRate.rateID] = [subRate];
+ }
+ return acc;
+ }, {} as Record);
+
+ // Copy the custom unit and remove the sub rates that are to be deleted
+ const newCustomUnit: CustomUnit = lodashDeepClone(customUnit);
+ const customUnitOnyxUpdate: DeletePerDiemCustomUnitOnyxType = lodashDeepClone(customUnit);
+ for (const rateID in mappedDeletedSubRatesToRate) {
+ if (!(rateID in newCustomUnit.rates)) {
+ // eslint-disable-next-line no-continue
+ continue;
+ }
+ const subRates = mappedDeletedSubRatesToRate[rateID];
+ if (subRates.length === newCustomUnit.rates[rateID].subRates?.length) {
+ delete newCustomUnit.rates[rateID];
+ customUnitOnyxUpdate.rates[rateID] = null;
+ } else {
+ const newSubRates = newCustomUnit.rates[rateID].subRates?.filter((subRate) => !subRates.some((subRateToBeDeleted) => subRateToBeDeleted.subRateID === subRate.id));
+ newCustomUnit.rates[rateID].subRates = newSubRates;
+ customUnitOnyxUpdate.rates[rateID] = {...customUnitOnyxUpdate.rates[rateID], subRates: newSubRates};
+ }
+ }
+ return {newCustomUnit, customUnitOnyxUpdate};
+}
+
+function deleteWorkspacePerDiemRates(policyID: string, customUnit: CustomUnit | undefined, subRatesToBeDeleted: SubRateData[]) {
+ if (!policyID || isEmptyObject(customUnit) || !subRatesToBeDeleted.length) {
+ return;
+ }
+ const {newCustomUnit, customUnitOnyxUpdate} = prepareNewCustomUnit(customUnit, subRatesToBeDeleted);
+ const onyxData: OnyxData = {
+ optimisticData: [
+ {
+ onyxMethod: Onyx.METHOD.MERGE,
+ key: `${ONYXKEYS.COLLECTION.POLICY}${policyID}`,
+ value: {
+ customUnits: {
+ [customUnit.customUnitID]: customUnitOnyxUpdate,
+ },
+ },
+ },
+ ],
+ };
+
+ const parameters = {
+ policyID,
+ customUnit: JSON.stringify(newCustomUnit),
+ };
+
+ API.write(WRITE_COMMANDS.UPDATE_WORKSPACE_CUSTOM_UNIT, parameters, onyxData);
+}
+
+function editPerDiemRateDestination(policyID: string, rateID: string, customUnit: CustomUnit | undefined, newDestination: string) {
+ if (!policyID || !rateID || isEmptyObject(customUnit) || !newDestination) {
+ return;
+ }
+
+ const newCustomUnit: CustomUnit = lodashDeepClone(customUnit);
+ newCustomUnit.rates[rateID].name = newDestination;
+
+ const onyxData: OnyxData = {
+ optimisticData: [
+ {
+ onyxMethod: Onyx.METHOD.MERGE,
+ key: `${ONYXKEYS.COLLECTION.POLICY}${policyID}`,
+ value: {
+ customUnits: {
+ [customUnit.customUnitID]: newCustomUnit,
+ },
+ },
+ },
+ ],
+ };
+
+ const parameters = {
+ policyID,
+ customUnit: JSON.stringify(newCustomUnit),
+ };
+
+ API.write(WRITE_COMMANDS.UPDATE_WORKSPACE_CUSTOM_UNIT, parameters, onyxData);
+}
+
+function editPerDiemRateSubrate(policyID: string, rateID: string, subRateID: string, customUnit: CustomUnit | undefined, newSubrate: string) {
+ if (!policyID || !rateID || isEmptyObject(customUnit) || !newSubrate) {
+ return;
+ }
+
+ const newCustomUnit: CustomUnit = lodashDeepClone(customUnit);
+ newCustomUnit.rates[rateID].subRates = newCustomUnit.rates[rateID].subRates?.map((subRate) => {
+ if (subRate.id === subRateID) {
+ return {...subRate, name: newSubrate};
+ }
+ return subRate;
+ });
+
+ const onyxData: OnyxData = {
+ optimisticData: [
+ {
+ onyxMethod: Onyx.METHOD.MERGE,
+ key: `${ONYXKEYS.COLLECTION.POLICY}${policyID}`,
+ value: {
+ customUnits: {
+ [customUnit.customUnitID]: newCustomUnit,
+ },
+ },
+ },
+ ],
+ };
+
+ const parameters = {
+ policyID,
+ customUnit: JSON.stringify(newCustomUnit),
+ };
+
+ API.write(WRITE_COMMANDS.UPDATE_WORKSPACE_CUSTOM_UNIT, parameters, onyxData);
+}
+
+function editPerDiemRateAmount(policyID: string, rateID: string, subRateID: string, customUnit: CustomUnit | undefined, newAmount: number) {
+ if (!policyID || !rateID || isEmptyObject(customUnit) || !newAmount) {
+ return;
+ }
+
+ const newCustomUnit: CustomUnit = lodashDeepClone(customUnit);
+ newCustomUnit.rates[rateID].subRates = newCustomUnit.rates[rateID].subRates?.map((subRate) => {
+ if (subRate.id === subRateID) {
+ return {...subRate, rate: newAmount};
+ }
+ return subRate;
+ });
+
+ const onyxData: OnyxData = {
+ optimisticData: [
+ {
+ onyxMethod: Onyx.METHOD.MERGE,
+ key: `${ONYXKEYS.COLLECTION.POLICY}${policyID}`,
+ value: {
+ customUnits: {
+ [customUnit.customUnitID]: newCustomUnit,
+ },
+ },
+ },
+ ],
+ };
+
+ const parameters = {
+ policyID,
+ customUnit: JSON.stringify(newCustomUnit),
+ };
+
+ API.write(WRITE_COMMANDS.UPDATE_WORKSPACE_CUSTOM_UNIT, parameters, onyxData);
+}
+
+function editPerDiemRateCurrency(policyID: string, rateID: string, customUnit: CustomUnit | undefined, newCurrency: string) {
+ if (!policyID || !rateID || isEmptyObject(customUnit) || !newCurrency) {
+ return;
+ }
+
+ const newCustomUnit: CustomUnit = lodashDeepClone(customUnit);
+ newCustomUnit.rates[rateID].currency = newCurrency;
+
+ const onyxData: OnyxData = {
+ optimisticData: [
+ {
+ onyxMethod: Onyx.METHOD.MERGE,
+ key: `${ONYXKEYS.COLLECTION.POLICY}${policyID}`,
+ value: {
+ customUnits: {
+ [customUnit.customUnitID]: newCustomUnit,
+ },
+ },
+ },
+ ],
+ };
+
+ const parameters = {
+ policyID,
+ customUnit: JSON.stringify(newCustomUnit),
+ };
+
+ API.write(WRITE_COMMANDS.UPDATE_WORKSPACE_CUSTOM_UNIT, parameters, onyxData);
+}
+
+export {
+ generateCustomUnitID,
+ enablePerDiem,
+ openPolicyPerDiemPage,
+ importPerDiemRates,
+ downloadPerDiemCSV,
+ clearPolicyPerDiemRatesErrorFields,
+ deleteWorkspacePerDiemRates,
+ editPerDiemRateDestination,
+ editPerDiemRateSubrate,
+ editPerDiemRateAmount,
+ editPerDiemRateCurrency,
+};
diff --git a/src/libs/actions/Session/index.ts b/src/libs/actions/Session/index.ts
index 8685a0363e31..1dbb01b008dd 100644
--- a/src/libs/actions/Session/index.ts
+++ b/src/libs/actions/Session/index.ts
@@ -17,7 +17,6 @@ import type {
RequestAccountValidationLinkParams,
RequestNewValidateCodeParams,
RequestUnlinkValidationLinkParams,
- ResetSMSDeliveryFailureParams,
SignInUserWithLinkParams,
SignUpUserParams,
UnlinkLoginParams,
@@ -1201,52 +1200,6 @@ function isUserOnPrivateDomain() {
return false;
}
-/**
- * To reset SMS delivery failure
- */
-function resetSMSDeliveryFailure(login: string) {
- const params: ResetSMSDeliveryFailureParams = {login};
-
- const optimisticData: OnyxUpdate[] = [
- {
- onyxMethod: Onyx.METHOD.MERGE,
- key: ONYXKEYS.ACCOUNT,
- value: {
- errors: null,
- smsDeliveryFailureStatus: {
- isLoading: true,
- },
- },
- },
- ];
- const successData: OnyxUpdate[] = [
- {
- onyxMethod: Onyx.METHOD.MERGE,
- key: ONYXKEYS.ACCOUNT,
- value: {
- smsDeliveryFailureStatus: {
- isLoading: false,
- isReset: true,
- },
- },
- },
- ];
- const failureData: OnyxUpdate[] = [
- {
- onyxMethod: Onyx.METHOD.MERGE,
- key: ONYXKEYS.ACCOUNT,
- value: {
- errors: ErrorUtils.getMicroSecondOnyxErrorWithTranslationKey('common.genericErrorMessage'),
- smsDeliveryFailureStatus: {
- isLoading: false,
- },
- },
- },
- ];
-
- API.write(WRITE_COMMANDS.RESET_SMS_DELIVERY_FAILURE, params, {optimisticData, successData, failureData});
-}
-
export {
beginSignIn,
beginAppleSignIn,
@@ -1286,5 +1239,4 @@ export {
signInAfterTransitionFromOldDot,
validateUserAndGetAccessiblePolicies,
isUserOnPrivateDomain,
- resetSMSDeliveryFailure,
};
diff --git a/src/libs/actions/TaxRate.ts b/src/libs/actions/TaxRate.ts
index 5d610e93bee7..c743e18b23fb 100644
--- a/src/libs/actions/TaxRate.ts
+++ b/src/libs/actions/TaxRate.ts
@@ -290,7 +290,7 @@ function deletePolicyTaxes(policyID: string, taxesToDelete: string[]) {
.sort((a, b) => a.localeCompare(b))
.at(0);
const distanceRateCustomUnit = PolicyUtils.getDistanceRateCustomUnit(policy);
- const customUnitID = distanceRateCustomUnit?.customUnitID ?? '-1';
+ const customUnitID = distanceRateCustomUnit?.customUnitID;
const ratesToUpdate = Object.values(distanceRateCustomUnit?.rates ?? {}).filter(
(rate) => !!rate.attributes?.taxRateExternalID && taxesToDelete.includes(rate.attributes?.taxRateExternalID),
);
@@ -303,11 +303,11 @@ function deletePolicyTaxes(policyID: string, taxesToDelete: string[]) {
const isForeignTaxRemoved = foreignTaxDefault && taxesToDelete.includes(foreignTaxDefault);
const optimisticRates: Record> = {};
- const successRates: Record = {};
- const failureRates: Record = {};
+ const successRates: Record> = {};
+ const failureRates: Record> = {};
ratesToUpdate.forEach((rate) => {
- const rateID = rate.customUnitRateID ?? '';
+ const rateID = rate.customUnitRateID;
optimisticRates[rateID] = {
attributes: {
taxRateExternalID: null,
@@ -343,11 +343,12 @@ function deletePolicyTaxes(policyID: string, taxesToDelete: string[]) {
return acc;
}, {}),
},
- customUnits: distanceRateCustomUnit && {
- [customUnitID]: {
- rates: optimisticRates,
+ customUnits: distanceRateCustomUnit &&
+ customUnitID && {
+ [customUnitID]: {
+ rates: optimisticRates,
+ },
},
- },
},
},
],
@@ -363,11 +364,12 @@ function deletePolicyTaxes(policyID: string, taxesToDelete: string[]) {
return acc;
}, {}),
},
- customUnits: distanceRateCustomUnit && {
- [customUnitID]: {
- rates: successRates,
+ customUnits: distanceRateCustomUnit &&
+ customUnitID && {
+ [customUnitID]: {
+ rates: successRates,
+ },
},
- },
},
},
],
@@ -387,11 +389,12 @@ function deletePolicyTaxes(policyID: string, taxesToDelete: string[]) {
return acc;
}, {}),
},
- customUnits: distanceRateCustomUnit && {
- [customUnitID]: {
- rates: failureRates,
+ customUnits: distanceRateCustomUnit &&
+ customUnitID && {
+ [customUnitID]: {
+ rates: failureRates,
+ },
},
- },
},
},
],
@@ -552,7 +555,7 @@ function setPolicyTaxCode(policyID: string, oldTaxCode: string, newTaxCode: stri
};
}
return rates;
- }, {} as Record),
+ }, {} as Record>),
},
};
const oldDefaultExternalID = policy?.taxRates?.defaultExternalID;
diff --git a/src/pages/PrivateNotes/PrivateNotesEditPage.tsx b/src/pages/PrivateNotes/PrivateNotesEditPage.tsx
index a0370ef6cbbd..4d084cfa924d 100644
--- a/src/pages/PrivateNotes/PrivateNotesEditPage.tsx
+++ b/src/pages/PrivateNotes/PrivateNotesEditPage.tsx
@@ -12,7 +12,6 @@ import type {AnimatedTextInputRef} from '@components/RNTextInput';
import ScreenWrapper from '@components/ScreenWrapper';
import Text from '@components/Text';
import TextInput from '@components/TextInput';
-import useHtmlPaste from '@hooks/useHtmlPaste';
import useLocalize from '@hooks/useLocalize';
import useThemeStyles from '@hooks/useThemeStyles';
import Navigation from '@libs/Navigation/Navigation';
@@ -66,8 +65,6 @@ function PrivateNotesEditPage({route, report, accountID}: PrivateNotesEditPagePr
const privateNotesInput = useRef(null);
const focusTimeoutRef = useRef(null);
- useHtmlPaste(privateNotesInput);
-
useFocusEffect(
useCallback(() => {
focusTimeoutRef.current = setTimeout(() => {
diff --git a/src/pages/TransactionDuplicate/Confirmation.tsx b/src/pages/TransactionDuplicate/Confirmation.tsx
index 520a253469db..8d973a262186 100644
--- a/src/pages/TransactionDuplicate/Confirmation.tsx
+++ b/src/pages/TransactionDuplicate/Confirmation.tsx
@@ -39,8 +39,8 @@ function Confirmation() {
const currentUserPersonalDetails = useCurrentUserPersonalDetails();
const [reviewDuplicates, reviewDuplicatesResult] = useOnyx(ONYXKEYS.REVIEW_DUPLICATES);
const transaction = useMemo(() => TransactionUtils.buildNewTransactionAfterReviewingDuplicates(reviewDuplicates), [reviewDuplicates]);
- const transactionID = TransactionUtils.getTransactionID(route.params.threadReportID ?? '');
- const compareResult = TransactionUtils.compareDuplicateTransactionFields(transactionID, reviewDuplicates?.reportID ?? '-1');
+ const transactionID = TransactionUtils.getTransactionID(route.params.threadReportID);
+ const compareResult = TransactionUtils.compareDuplicateTransactionFields(transactionID, reviewDuplicates?.reportID);
const {goBack} = useReviewDuplicatesNavigation(Object.keys(compareResult.change ?? {}), 'confirmation', route.params.threadReportID, route.params.backTo);
const [report, reportResult] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${route.params.threadReportID}`);
const [iouReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${transaction?.reportID}`);
@@ -54,12 +54,15 @@ function Confirmation() {
const mergeDuplicates = useCallback(() => {
IOU.mergeDuplicates(transactionsMergeParams);
- Navigation.navigate(ROUTES.REPORT_WITH_ID.getRoute(reportAction?.childReportID ?? '-1'));
+ if (!reportAction?.childReportID) {
+ return;
+ }
+ Navigation.navigate(ROUTES.REPORT_WITH_ID.getRoute(reportAction?.childReportID));
}, [reportAction?.childReportID, transactionsMergeParams]);
const resolveDuplicates = useCallback(() => {
IOU.resolveDuplicates(transactionsMergeParams);
- Navigation.dismissModal(reportAction?.childReportID ?? '-1');
+ Navigation.dismissModal(reportAction?.childReportID);
}, [transactionsMergeParams, reportAction?.childReportID]);
const contextValue = useMemo(
@@ -75,8 +78,8 @@ function Confirmation() {
[report, reportAction],
);
- const reportTransactionID = TransactionUtils.getTransactionID(report?.reportID ?? '');
- const doesTransactionBelongToReport = reviewDuplicates?.transactionID === reportTransactionID || reviewDuplicates?.duplicates.includes(reportTransactionID);
+ const reportTransactionID = report?.reportID ? TransactionUtils.getTransactionID(report.reportID) : undefined;
+ const doesTransactionBelongToReport = reviewDuplicates?.transactionID === reportTransactionID || (reportTransactionID && reviewDuplicates?.duplicates.includes(reportTransactionID));
// eslint-disable-next-line rulesdir/no-negated-variables
const shouldShowNotFoundPage =
diff --git a/src/pages/iou/request/step/IOURequestStepCategory.tsx b/src/pages/iou/request/step/IOURequestStepCategory.tsx
index 1365a555d197..87a56e977817 100644
--- a/src/pages/iou/request/step/IOURequestStepCategory.tsx
+++ b/src/pages/iou/request/step/IOURequestStepCategory.tsx
@@ -43,7 +43,7 @@ function IOURequestStepCategory({
},
transaction,
}: IOURequestStepCategoryProps) {
- const [splitDraftTransaction] = useOnyx(`${ONYXKEYS.COLLECTION.SPLIT_TRANSACTION_DRAFT}${transactionID ?? '-1'}`);
+ const [splitDraftTransaction] = useOnyx(`${ONYXKEYS.COLLECTION.SPLIT_TRANSACTION_DRAFT}${transactionID}`);
const [policyReal] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY}${IOU.getIOURequestPolicyID(transaction, reportReal)}`);
const [policyDraft] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY_DRAFTS}${IOU.getIOURequestPolicyID(transaction, reportDraft)}`);
const [policyCategoriesReal] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${IOU.getIOURequestPolicyID(transaction, reportReal)}`);
@@ -68,7 +68,8 @@ function IOURequestStepCategory({
const {translate} = useLocalize();
const isEditing = action === CONST.IOU.ACTION.EDIT;
const isEditingSplitBill = isEditing && iouType === CONST.IOU.TYPE.SPLIT;
- const transactionCategory = ReportUtils.getTransactionDetails(isEditingSplitBill && !lodashIsEmpty(splitDraftTransaction) ? splitDraftTransaction : transaction)?.category;
+ const currentTransaction = isEditingSplitBill && !lodashIsEmpty(splitDraftTransaction) ? splitDraftTransaction : transaction;
+ const transactionCategory = ReportUtils.getTransactionDetails(currentTransaction)?.category;
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
const reportAction = reportActions?.[report?.parentReportActionID || reportActionID] ?? null;
@@ -85,11 +86,11 @@ function IOURequestStepCategory({
const shouldShowNotFoundPage = isEditing && (isSplitBill ? !canEditSplitBill : !ReportActionsUtils.isMoneyRequestAction(reportAction) || !ReportUtils.canEditMoneyRequest(reportAction));
const fetchData = () => {
- if (policy && policyCategories) {
+ if ((!!policy && !!policyCategories) || !report?.policyID) {
return;
}
- Category.getPolicyCategories(report?.policyID ?? '-1');
+ Category.getPolicyCategories(report?.policyID);
};
const {isOffline} = useNetwork({onReconnect: fetchData});
const isLoading = !isOffline && policyCategories === undefined;
@@ -113,7 +114,7 @@ function IOURequestStepCategory({
if (transaction) {
// In the split flow, when editing we use SPLIT_TRANSACTION_DRAFT to save draft value
if (isEditingSplitBill) {
- IOU.setDraftSplitTransaction(transaction.transactionID, {category: updatedCategory});
+ IOU.setDraftSplitTransaction(transaction.transactionID, {category: updatedCategory}, policy);
navigateBack();
return;
}
@@ -125,10 +126,12 @@ function IOURequestStepCategory({
}
}
- IOU.setMoneyRequestCategory(transactionID, updatedCategory);
+ IOU.setMoneyRequestCategory(transactionID, updatedCategory, policy?.id);
if (action === CONST.IOU.ACTION.CATEGORIZE) {
- Navigation.navigate(ROUTES.MONEY_REQUEST_STEP_CONFIRMATION.getRoute(action, iouType, transactionID, report?.reportID ?? '-1'));
+ if (report?.reportID) {
+ Navigation.navigate(ROUTES.MONEY_REQUEST_STEP_CONFIRMATION.getRoute(action, iouType, transactionID, report.reportID));
+ }
return;
}
@@ -168,14 +171,18 @@ function IOURequestStepCategory({
success
style={[styles.w100]}
onPress={() => {
- if (!policy?.areCategoriesEnabled) {
- Category.enablePolicyCategories(policy?.id ?? '-1', true, false);
+ if (!policy?.id || !report?.reportID) {
+ return;
+ }
+
+ if (!policy.areCategoriesEnabled) {
+ Category.enablePolicyCategories(policy.id, true, false);
}
InteractionManager.runAfterInteractions(() => {
Navigation.navigate(
ROUTES.SETTINGS_CATEGORIES_ROOT.getRoute(
- policy?.id ?? '-1',
- ROUTES.MONEY_REQUEST_STEP_CATEGORY.getRoute(action, iouType, transactionID, report?.reportID ?? '-1', backTo, reportActionID),
+ policy.id,
+ ROUTES.MONEY_REQUEST_STEP_CATEGORY.getRoute(action, iouType, transactionID, report.reportID, backTo, reportActionID),
),
);
});
@@ -192,7 +199,7 @@ function IOURequestStepCategory({
{translate('iou.categorySelection')}
>
diff --git a/src/pages/iou/request/step/IOURequestStepConfirmation.tsx b/src/pages/iou/request/step/IOURequestStepConfirmation.tsx
index 15aef60bf3d3..6083727cf2ad 100644
--- a/src/pages/iou/request/step/IOURequestStepConfirmation.tsx
+++ b/src/pages/iou/request/step/IOURequestStepConfirmation.tsx
@@ -97,7 +97,7 @@ function IOURequestStepConfirmation({
return {
login: participant?.login ?? '',
- accountID: participant?.accountID ?? -1,
+ accountID: participant?.accountID ?? CONST.DEFAULT_NUMBER_ID,
avatar: Expensicons.FallbackAvatar,
displayName: participant?.login ?? '',
isOptimisticPersonalDetail: true,
@@ -156,9 +156,9 @@ function IOURequestStepConfirmation({
return;
}
if (policyCategories?.[transaction.category] && !policyCategories[transaction.category].enabled) {
- IOU.setMoneyRequestCategory(transactionID, '');
+ IOU.setMoneyRequestCategory(transactionID, '', policy?.id);
}
- }, [policyCategories, transaction?.category, transactionID]);
+ }, [policy?.id, policyCategories, transaction?.category, transactionID]);
const policyDistance = Object.values(policy?.customUnits ?? {}).find((customUnit) => customUnit.name === CONST.CUSTOM_UNITS.NAME_DISTANCE);
const defaultCategory = policyDistance?.defaultCategory ?? '';
@@ -167,10 +167,10 @@ function IOURequestStepConfirmation({
if (requestType !== CONST.IOU.REQUEST_TYPE.DISTANCE || !!transaction?.category) {
return;
}
- IOU.setMoneyRequestCategory(transactionID, defaultCategory);
+ IOU.setMoneyRequestCategory(transactionID, defaultCategory, policy?.id);
// Prevent resetting to default when unselect category
// eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps
- }, [transactionID, requestType, defaultCategory]);
+ }, [transactionID, requestType, defaultCategory, policy?.id]);
const navigateBack = useCallback(() => {
// If the action is categorize and there's no policies other than personal one, we simply call goBack(), i.e: dismiss the whole flow together
@@ -363,7 +363,9 @@ function IOURequestStepConfirmation({
.filter((accountID: string): boolean => (transaction?.splitShares?.[Number(accountID)]?.amount ?? 0) > 0)
.map((accountID) => Number(accountID));
splitParticipants = selectedParticipants.filter((participant) =>
- participantsWithAmount.includes(participant.isPolicyExpenseChat ? participant?.ownerAccountID ?? -1 : participant.accountID ?? -1),
+ participantsWithAmount.includes(
+ participant.isPolicyExpenseChat ? participant?.ownerAccountID ?? CONST.DEFAULT_NUMBER_ID : participant.accountID ?? CONST.DEFAULT_NUMBER_ID,
+ ),
);
}
const trimmedComment = transaction?.comment?.comment?.trim() ?? '';
diff --git a/src/pages/settings/Wallet/Card/GetPhysicalCardPhone.tsx b/src/pages/settings/Wallet/Card/GetPhysicalCardPhone.tsx
index bcb3fe646fff..075bc4a3ff5c 100644
--- a/src/pages/settings/Wallet/Card/GetPhysicalCardPhone.tsx
+++ b/src/pages/settings/Wallet/Card/GetPhysicalCardPhone.tsx
@@ -1,3 +1,4 @@
+import {Str} from 'expensify-common';
import React from 'react';
import {View} from 'react-native';
import {useOnyx} from 'react-native-onyx';
@@ -8,6 +9,8 @@ import useLocalize from '@hooks/useLocalize';
import useThemeStyles from '@hooks/useThemeStyles';
import * as LoginUtils from '@libs/LoginUtils';
import type {PlatformStackScreenProps} from '@libs/Navigation/PlatformStackNavigation/types';
+import * as PhoneNumberUtils from '@libs/PhoneNumber';
+import * as ValidationUtils from '@libs/ValidationUtils';
import type {SettingsNavigatorParamList} from '@navigation/types';
import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
@@ -40,12 +43,17 @@ function GetPhysicalCardPhone({
const errors: OnValidateResult = {};
- if (!LoginUtils.validateNumber(phoneNumberToValidate)) {
- errors.phoneNumber = translate('common.error.phoneNumber');
- } else if (!phoneNumberToValidate) {
+ if (!ValidationUtils.isRequiredFulfilled(phoneNumberToValidate)) {
errors.phoneNumber = translate('common.error.fieldRequired');
}
+ const phoneNumberWithCountryCode = LoginUtils.appendCountryCode(phoneNumberToValidate);
+ const parsedPhoneNumber = PhoneNumberUtils.parsePhoneNumber(phoneNumberWithCountryCode);
+
+ if (!parsedPhoneNumber.possible || !Str.isValidE164Phone(phoneNumberWithCountryCode.slice(0))) {
+ errors.phoneNumber = translate('bankAccount.error.phoneNumber');
+ }
+
return errors;
};
diff --git a/src/pages/signin/SMSDeliveryFailurePage.tsx b/src/pages/signin/SMSDeliveryFailurePage.tsx
index 8361c3614d40..cb9c7674d60e 100644
--- a/src/pages/signin/SMSDeliveryFailurePage.tsx
+++ b/src/pages/signin/SMSDeliveryFailurePage.tsx
@@ -3,12 +3,10 @@ import React, {useEffect, useMemo} from 'react';
import {Keyboard, View} from 'react-native';
import {useOnyx} from 'react-native-onyx';
import Button from '@components/Button';
-import FormAlertWithSubmitButton from '@components/FormAlertWithSubmitButton';
import Text from '@components/Text';
import useKeyboardState from '@hooks/useKeyboardState';
import useLocalize from '@hooks/useLocalize';
import useThemeStyles from '@hooks/useThemeStyles';
-import * as ErrorUtils from '@libs/ErrorUtils';
import * as Session from '@userActions/Session';
import ONYXKEYS from '@src/ONYXKEYS';
import ChangeExpensifyLoginLink from './ChangeExpensifyLoginLink';
@@ -29,11 +27,6 @@ function SMSDeliveryFailurePage() {
}, [credentials?.login]);
const SMSDeliveryFailureMessage = account?.smsDeliveryFailureStatus?.message;
- const hasSMSDeliveryFailure = account?.smsDeliveryFailureStatus?.hasSMSDeliveryFailure;
- const isReset = account?.smsDeliveryFailureStatus?.isReset;
-
- const errorText = useMemo(() => (account ? ErrorUtils.getLatestErrorMessage(account) : ''), [account]);
- const shouldShowError = !!errorText;
useEffect(() => {
if (!isKeyboardShown) {
@@ -42,79 +35,22 @@ function SMSDeliveryFailurePage() {
Keyboard.dismiss();
}, [isKeyboardShown]);
- if (hasSMSDeliveryFailure && isReset) {
- return (
- <>
-
-
-
- {translate('smsDeliveryFailurePage.validationFailed')} {SMSDeliveryFailureMessage}
-
-
-
-
- Session.clearSignInData()}
- pressOnEnter
- style={styles.w100}
- />
-
-
- Session.clearSignInData()} />
-
-
-
-
- >
- );
- }
-
- if (!hasSMSDeliveryFailure && isReset) {
- return (
- <>
-
-
- {translate('smsDeliveryFailurePage.validationSuccess')}
-
-
-
- Session.beginSignIn(login)}
- message={errorText}
- isAlertVisible={shouldShowError}
- containerStyles={[styles.w100, styles.mh0]}
- />
-
-
- Session.clearSignInData()} />
-
-
-
-
- >
- );
- }
-
return (
<>
- {translate('smsDeliveryFailurePage.smsDeliveryFailureMessage', {login})}
+
+ {translate('smsDeliveryFailurePage.smsDeliveryFailureMessage', {login})} {SMSDeliveryFailureMessage}
+
- Session.resetSMSDeliveryFailure(login)}
- message={errorText}
- isAlertVisible={shouldShowError}
- containerStyles={[styles.w100, styles.mh0]}
+ Session.clearSignInData()}
+ pressOnEnter
/>
diff --git a/src/pages/signin/SignInPage.tsx b/src/pages/signin/SignInPage.tsx
index 9cd3166ac3a1..e99b2c1d7fca 100644
--- a/src/pages/signin/SignInPage.tsx
+++ b/src/pages/signin/SignInPage.tsx
@@ -94,7 +94,7 @@ function getRenderOptions({
const isSAMLEnabled = !!account?.isSAMLEnabled;
const isSAMLRequired = !!account?.isSAMLRequired;
const hasEmailDeliveryFailure = !!account?.hasEmailDeliveryFailure;
- const hasSMSDeliveryFailure = !!account?.smsDeliveryFailureStatus;
+ const hasSMSDeliveryFailure = !!account?.smsDeliveryFailureStatus?.hasSMSDeliveryFailure;
// True, if the user has SAML required, and we haven't yet initiated SAML for their account
const shouldInitiateSAMLLogin = hasAccount && hasLogin && isSAMLRequired && !hasInitiatedSAMLLogin && !!account.isLoading;
@@ -111,7 +111,7 @@ function getRenderOptions({
const shouldShouldSignUpWelcomeForm = !!credentials?.login && !account?.validated && !account?.accountExists && !account?.domainControlled;
const shouldShowLoginForm = !shouldShowAnotherLoginPageOpenedMessage && !hasLogin && !hasValidateCode;
const shouldShowEmailDeliveryFailurePage = hasLogin && hasEmailDeliveryFailure && !shouldShowChooseSSOOrMagicCode && !shouldInitiateSAMLLogin;
- const shouldShowSMSDeliveryFailurePage = !!(hasLogin && hasSMSDeliveryFailure && !shouldShowChooseSSOOrMagicCode && !shouldInitiateSAMLLogin && account?.accountExists);
+ const shouldShowSMSDeliveryFailurePage = hasLogin && hasSMSDeliveryFailure && !shouldShowChooseSSOOrMagicCode && !shouldInitiateSAMLLogin;
const isUnvalidatedSecondaryLogin = hasLogin && !isPrimaryLogin && !account?.validated && !hasEmailDeliveryFailure && !hasSMSDeliveryFailure;
const shouldShowValidateCodeForm =
!shouldShouldSignUpWelcomeForm &&
diff --git a/src/pages/workspace/categories/WorkspaceCategoriesPage.tsx b/src/pages/workspace/categories/WorkspaceCategoriesPage.tsx
index 737fbc2972c1..a8a37638f87e 100644
--- a/src/pages/workspace/categories/WorkspaceCategoriesPage.tsx
+++ b/src/pages/workspace/categories/WorkspaceCategoriesPage.tsx
@@ -1,4 +1,4 @@
-import {useFocusEffect, useIsFocused} from '@react-navigation/native';
+import {useFocusEffect} from '@react-navigation/native';
import lodashSortBy from 'lodash/sortBy';
import React, {useCallback, useEffect, useMemo, useState} from 'react';
import {ActivityIndicator, View} from 'react-native';
@@ -23,6 +23,7 @@ import TableListItemSkeleton from '@components/Skeletons/TableRowSkeleton';
import Text from '@components/Text';
import TextLink from '@components/TextLink';
import useAutoTurnSelectionModeOffWhenHasNoActiveOption from '@hooks/useAutoTurnSelectionModeOffWhenHasNoActiveOption';
+import useCleanupSelectedOptions from '@hooks/useCleanupSelectedOptions';
import useEnvironment from '@hooks/useEnvironment';
import useLocalize from '@hooks/useLocalize';
import useMobileSelectionMode from '@hooks/useMobileSelectionMode';
@@ -70,7 +71,6 @@ function WorkspaceCategoriesPage({route}: WorkspaceCategoriesPageProps) {
const [selectedCategories, setSelectedCategories] = useState>({});
const [isDownloadFailureModalVisible, setIsDownloadFailureModalVisible] = useState(false);
const [deleteCategoriesConfirmModalVisible, setDeleteCategoriesConfirmModalVisible] = useState(false);
- const isFocused = useIsFocused();
const {environmentURL} = useEnvironment();
const policyId = route.params.policyID ?? '-1';
const backTo = route.params?.backTo;
@@ -98,12 +98,8 @@ function WorkspaceCategoriesPage({route}: WorkspaceCategoriesPageProps) {
}, [fetchCategories]),
);
- useEffect(() => {
- if (isFocused) {
- return;
- }
- setSelectedCategories({});
- }, [isFocused]);
+ const cleanupSelectedOption = useCallback(() => setSelectedCategories({}), []);
+ useCleanupSelectedOptions(cleanupSelectedOption);
const categoryList = useMemo(
() =>
@@ -151,6 +147,10 @@ function WorkspaceCategoriesPage({route}: WorkspaceCategoriesPageProps) {
};
const navigateToCategorySettings = (category: PolicyOption) => {
+ if (isSmallScreenWidth && selectionMode?.isEnabled) {
+ toggleCategory(category);
+ return;
+ }
Navigation.navigate(
isQuickSettingsFlow
? ROUTES.SETTINGS_CATEGORY_SETTINGS.getRoute(policyId, category.keyForList, backTo)
diff --git a/src/pages/workspace/distanceRates/PolicyDistanceRateEditPage.tsx b/src/pages/workspace/distanceRates/PolicyDistanceRateEditPage.tsx
index 6ecff25561f5..2266a2254d40 100644
--- a/src/pages/workspace/distanceRates/PolicyDistanceRateEditPage.tsx
+++ b/src/pages/workspace/distanceRates/PolicyDistanceRateEditPage.tsx
@@ -43,7 +43,7 @@ function PolicyDistanceRateEditPage({route}: PolicyDistanceRateEditPageProps) {
Navigation.goBack();
return;
}
- if (!customUnit) {
+ if (!customUnit || !rate) {
return;
}
DistanceRate.updatePolicyDistanceRateValue(policyID, customUnit, [{...rate, rate: Number(values.rate) * CONST.POLICY.CUSTOM_UNIT_RATE_BASE_OFFSET}]);
diff --git a/src/pages/workspace/distanceRates/PolicyDistanceRateTaxRateEditPage.tsx b/src/pages/workspace/distanceRates/PolicyDistanceRateTaxRateEditPage.tsx
index cee93bb0f9c9..06a8faf29c1d 100644
--- a/src/pages/workspace/distanceRates/PolicyDistanceRateTaxRateEditPage.tsx
+++ b/src/pages/workspace/distanceRates/PolicyDistanceRateTaxRateEditPage.tsx
@@ -28,14 +28,14 @@ function PolicyDistanceRateTaxRateEditPage({route, policy}: PolicyDistanceRateTa
const customUnit = getDistanceRateCustomUnit(policy);
const rate = customUnit?.rates[rateID];
const taxRateExternalID = rate?.attributes?.taxRateExternalID;
- const selectedTaxRate = TransactionUtils.getWorkspaceTaxesSettingsName(policy, taxRateExternalID ?? '');
+ const selectedTaxRate = taxRateExternalID ? TransactionUtils.getWorkspaceTaxesSettingsName(policy, taxRateExternalID) : undefined;
const onTaxRateChange = (newTaxRate: TaxOptionsListUtils.TaxRatesOption) => {
if (taxRateExternalID === newTaxRate.code) {
Navigation.goBack();
return;
}
- if (!customUnit) {
+ if (!customUnit || !rate) {
return;
}
DistanceRate.updateDistanceTaxRate(policyID, customUnit, [
diff --git a/src/pages/workspace/distanceRates/PolicyDistanceRateTaxReclaimableEditPage.tsx b/src/pages/workspace/distanceRates/PolicyDistanceRateTaxReclaimableEditPage.tsx
index bb45e0273a5e..fd62217f6572 100644
--- a/src/pages/workspace/distanceRates/PolicyDistanceRateTaxReclaimableEditPage.tsx
+++ b/src/pages/workspace/distanceRates/PolicyDistanceRateTaxReclaimableEditPage.tsx
@@ -43,7 +43,7 @@ function PolicyDistanceRateTaxReclaimableEditPage({route, policy}: PolicyDistanc
Navigation.goBack();
return;
}
- if (!customUnit) {
+ if (!customUnit || !rate) {
return;
}
DistanceRate.updateDistanceTaxClaimableValue(policyID, customUnit, [
diff --git a/src/pages/workspace/invoices/WorkspaceInvoiceVBASection.tsx b/src/pages/workspace/invoices/WorkspaceInvoiceVBASection.tsx
index 4761ad176940..ce3c2d0fe29f 100644
--- a/src/pages/workspace/invoices/WorkspaceInvoiceVBASection.tsx
+++ b/src/pages/workspace/invoices/WorkspaceInvoiceVBASection.tsx
@@ -20,6 +20,7 @@ import * as PaymentUtils from '@libs/PaymentUtils';
import PaymentMethodList from '@pages/settings/Wallet/PaymentMethodList';
import variables from '@styles/variables';
import * as BankAccounts from '@userActions/BankAccounts';
+import * as Modal from '@userActions/Modal';
import * as PaymentMethods from '@userActions/PaymentMethods';
import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
@@ -117,7 +118,7 @@ function WorkspaceInvoiceVBASection({policyID}: WorkspaceInvoiceVBASectionProps)
selectedPaymentMethod: account ?? {},
selectedPaymentMethodType: accountType,
formattedSelectedPaymentMethod,
- methodID: methodID ?? '-1',
+ methodID: methodID ?? CONST.DEFAULT_NUMBER_ID,
});
setShouldShowDefaultDeleteMenu(true);
setMenuPosition();
@@ -155,7 +156,7 @@ function WorkspaceInvoiceVBASection({policyID}: WorkspaceInvoiceVBASectionProps)
const previousPaymentMethod = paymentMethods.find((method) => !!method.isDefault);
const currentPaymentMethod = paymentMethods.find((method) => method.methodID === paymentMethod.methodID);
if (paymentMethod.selectedPaymentMethodType === CONST.PAYMENT_METHODS.PERSONAL_BANK_ACCOUNT) {
- PaymentMethods.setInvoicingTransferBankAccount(currentPaymentMethod?.methodID ?? -1, policyID, previousPaymentMethod?.methodID ?? -1);
+ PaymentMethods.setInvoicingTransferBankAccount(currentPaymentMethod?.methodID ?? CONST.DEFAULT_NUMBER_ID, policyID, previousPaymentMethod?.methodID ?? CONST.DEFAULT_NUMBER_ID);
}
}, [bankAccountList, styles, paymentMethod.selectedPaymentMethodType, paymentMethod.methodID, policyID]);
@@ -231,27 +232,27 @@ function WorkspaceInvoiceVBASection({policyID}: WorkspaceInvoiceVBASectionProps)
setShowConfirmDeleteModal(true)}
+ onPress={() => Modal.close(() => setShowConfirmDeleteModal(true))}
wrapperStyle={[styles.pv3, styles.ph5, !shouldUseNarrowLayout ? styles.sidebarPopover : {}]}
/>
)}
- {
- deletePaymentMethod();
- hideDefaultDeleteMenu();
- }}
- onCancel={hideDefaultDeleteMenu}
- title={translate('walletPage.deleteAccount')}
- prompt={translate('walletPage.deleteConfirmation')}
- confirmText={translate('common.delete')}
- cancelText={translate('common.cancel')}
- shouldShowCancelButton
- danger
- onModalHide={resetSelectedPaymentMethodData}
- />
+ {
+ deletePaymentMethod();
+ hideDefaultDeleteMenu();
+ }}
+ onCancel={hideDefaultDeleteMenu}
+ title={translate('walletPage.deleteAccount')}
+ prompt={translate('walletPage.deleteConfirmation')}
+ confirmText={translate('common.delete')}
+ cancelText={translate('common.cancel')}
+ shouldShowCancelButton
+ danger
+ onModalHide={resetSelectedPaymentMethodData}
+ />
;
+
+function EditPerDiemAmountPage({route}: EditPerDiemAmountPageProps) {
+ const styles = useThemeStyles();
+ const {translate} = useLocalize();
+ const policyID = route.params.policyID;
+ const rateID = route.params.rateID;
+ const subRateID = route.params.subRateID;
+ const [policy] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY}${policyID}`);
+
+ const customUnit = getPerDiemCustomUnit(policy);
+
+ const selectedRate = customUnit?.rates?.[rateID];
+ const selectedSubrate = selectedRate?.subRates?.find((subRate) => subRate.id === subRateID);
+
+ const defaultAmount = selectedSubrate?.rate ? convertToFrontendAmountAsString(Number(selectedSubrate.rate)) : undefined;
+
+ const {inputCallbackRef} = useAutoFocusInput();
+
+ const validate = useCallback(
+ (values: FormOnyxValues): FormInputErrors => {
+ const errors: FormInputErrors = {};
+
+ const newAmount = values.amount.trim();
+ const backendAmount = newAmount ? convertToBackendAmount(Number(newAmount)) : 0;
+
+ if (backendAmount === 0) {
+ errors.amount = translate('common.error.fieldRequired');
+ }
+
+ return errors;
+ },
+ [translate],
+ );
+
+ const editAmount = useCallback(
+ (values: FormOnyxValues) => {
+ const newAmount = values.amount.trim();
+ const backendAmount = newAmount ? convertToBackendAmount(Number(newAmount)) : 0;
+ if (backendAmount !== Number(selectedSubrate?.rate)) {
+ PerDiem.editPerDiemRateAmount(policyID, rateID, subRateID, customUnit, backendAmount);
+ }
+ Navigation.goBack(ROUTES.WORKSPACE_PER_DIEM_DETAILS.getRoute(policyID, rateID, subRateID));
+ },
+ [selectedSubrate?.rate, policyID, rateID, subRateID, customUnit],
+ );
+
+ return (
+
+
+ Navigation.goBack(ROUTES.WORKSPACE_PER_DIEM_DETAILS.getRoute(policyID, rateID, subRateID))}
+ />
+
+
+
+
+
+ );
+}
+
+EditPerDiemAmountPage.displayName = 'EditPerDiemAmountPage';
+
+export default EditPerDiemAmountPage;
diff --git a/src/pages/workspace/perDiem/EditPerDiemCurrencyPage.tsx b/src/pages/workspace/perDiem/EditPerDiemCurrencyPage.tsx
new file mode 100644
index 000000000000..a12da5474f12
--- /dev/null
+++ b/src/pages/workspace/perDiem/EditPerDiemCurrencyPage.tsx
@@ -0,0 +1,78 @@
+import React, {useCallback} from 'react';
+import {View} from 'react-native';
+import {useOnyx} from 'react-native-onyx';
+import CurrencySelectionList from '@components/CurrencySelectionList';
+import type {CurrencyListItem} from '@components/CurrencySelectionList/types';
+import HeaderWithBackButton from '@components/HeaderWithBackButton';
+import ScreenWrapper from '@components/ScreenWrapper';
+import Text from '@components/Text';
+import useLocalize from '@hooks/useLocalize';
+import useThemeStyles from '@hooks/useThemeStyles';
+import Navigation from '@libs/Navigation/Navigation';
+import type {PlatformStackScreenProps} from '@libs/Navigation/PlatformStackNavigation/types';
+import {getPerDiemCustomUnit} from '@libs/PolicyUtils';
+import type {SettingsNavigatorParamList} from '@navigation/types';
+import AccessOrNotFoundWrapper from '@pages/workspace/AccessOrNotFoundWrapper';
+import * as PerDiem from '@userActions/Policy/PerDiem';
+import CONST from '@src/CONST';
+import ONYXKEYS from '@src/ONYXKEYS';
+import ROUTES from '@src/ROUTES';
+import type SCREENS from '@src/SCREENS';
+import {isEmptyObject} from '@src/types/utils/EmptyObject';
+
+type EditPerDiemCurrencyPageProps = PlatformStackScreenProps;
+
+function EditPerDiemCurrencyPage({route}: EditPerDiemCurrencyPageProps) {
+ const styles = useThemeStyles();
+ const {translate} = useLocalize();
+ const policyID = route.params.policyID;
+ const rateID = route.params.rateID;
+ const subRateID = route.params.subRateID;
+ const [policy] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY}${policyID}`);
+
+ const customUnit = getPerDiemCustomUnit(policy);
+
+ const selectedRate = customUnit?.rates?.[rateID];
+
+ const editCurrency = useCallback(
+ (item: CurrencyListItem) => {
+ const newCurrency = item.currencyCode;
+ if (newCurrency !== selectedRate?.currency) {
+ PerDiem.editPerDiemRateCurrency(policyID, rateID, customUnit, newCurrency);
+ }
+ Navigation.goBack(ROUTES.WORKSPACE_PER_DIEM_DETAILS.getRoute(policyID, rateID, subRateID));
+ },
+ [selectedRate?.currency, policyID, rateID, subRateID, customUnit],
+ );
+
+ return (
+
+
+ Navigation.goBack(ROUTES.WORKSPACE_PER_DIEM_DETAILS.getRoute(policyID, rateID, subRateID))}
+ />
+
+ {translate('workspace.perDiem.editCurrencySubtitle', {destination: selectedRate?.name ?? ''})}
+
+
+
+
+ );
+}
+
+EditPerDiemCurrencyPage.displayName = 'EditPerDiemCurrencyPage';
+
+export default EditPerDiemCurrencyPage;
diff --git a/src/pages/workspace/perDiem/EditPerDiemDestinationPage.tsx b/src/pages/workspace/perDiem/EditPerDiemDestinationPage.tsx
new file mode 100644
index 000000000000..5bbe98a7b20a
--- /dev/null
+++ b/src/pages/workspace/perDiem/EditPerDiemDestinationPage.tsx
@@ -0,0 +1,115 @@
+import React, {useCallback} from 'react';
+import {View} from 'react-native';
+import {useOnyx} 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 HeaderWithBackButton from '@components/HeaderWithBackButton';
+import ScreenWrapper from '@components/ScreenWrapper';
+import Text from '@components/Text';
+import TextInput from '@components/TextInput';
+import useAutoFocusInput from '@hooks/useAutoFocusInput';
+import useLocalize from '@hooks/useLocalize';
+import useThemeStyles from '@hooks/useThemeStyles';
+import Navigation from '@libs/Navigation/Navigation';
+import type {PlatformStackScreenProps} from '@libs/Navigation/PlatformStackNavigation/types';
+import {getPerDiemCustomUnit} from '@libs/PolicyUtils';
+import type {SettingsNavigatorParamList} from '@navigation/types';
+import AccessOrNotFoundWrapper from '@pages/workspace/AccessOrNotFoundWrapper';
+import * as PerDiem from '@userActions/Policy/PerDiem';
+import CONST from '@src/CONST';
+import ONYXKEYS from '@src/ONYXKEYS';
+import ROUTES from '@src/ROUTES';
+import type SCREENS from '@src/SCREENS';
+import INPUT_IDS from '@src/types/form/WorkspacePerDiemForm';
+import {isEmptyObject} from '@src/types/utils/EmptyObject';
+
+type EditPerDiemDestinationPageProps = PlatformStackScreenProps;
+
+function EditPerDiemDestinationPage({route}: EditPerDiemDestinationPageProps) {
+ const styles = useThemeStyles();
+ const {translate} = useLocalize();
+ const policyID = route.params.policyID;
+ const rateID = route.params.rateID;
+ const subRateID = route.params.subRateID;
+ const [policy] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY}${policyID}`);
+
+ const customUnit = getPerDiemCustomUnit(policy);
+
+ const selectedRate = customUnit?.rates?.[rateID];
+
+ const {inputCallbackRef} = useAutoFocusInput();
+
+ const validate = useCallback(
+ (values: FormOnyxValues): FormInputErrors => {
+ const errors: FormInputErrors = {};
+
+ if (!values.destination.trim()) {
+ errors.destination = translate('common.error.fieldRequired');
+ }
+
+ return errors;
+ },
+ [translate],
+ );
+
+ const editDestination = useCallback(
+ (values: FormOnyxValues) => {
+ const newDestination = values.destination.trim();
+ if (newDestination !== selectedRate?.name) {
+ PerDiem.editPerDiemRateDestination(policyID, rateID, customUnit, newDestination);
+ }
+ Navigation.goBack(ROUTES.WORKSPACE_PER_DIEM_DETAILS.getRoute(policyID, rateID, subRateID));
+ },
+ [selectedRate?.name, policyID, rateID, subRateID, customUnit],
+ );
+
+ return (
+
+
+ Navigation.goBack(ROUTES.WORKSPACE_PER_DIEM_DETAILS.getRoute(policyID, rateID, subRateID))}
+ />
+
+
+
+ {translate('workspace.perDiem.editDestinationSubtitle', {destination: selectedRate?.name ?? ''})}
+
+
+
+
+
+
+ );
+}
+
+EditPerDiemDestinationPage.displayName = 'EditPerDiemDestinationPage';
+
+export default EditPerDiemDestinationPage;
diff --git a/src/pages/workspace/perDiem/EditPerDiemSubratePage.tsx b/src/pages/workspace/perDiem/EditPerDiemSubratePage.tsx
new file mode 100644
index 000000000000..413c8b3874f5
--- /dev/null
+++ b/src/pages/workspace/perDiem/EditPerDiemSubratePage.tsx
@@ -0,0 +1,109 @@
+import React, {useCallback} from 'react';
+import {useOnyx} 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 HeaderWithBackButton from '@components/HeaderWithBackButton';
+import ScreenWrapper from '@components/ScreenWrapper';
+import TextInput from '@components/TextInput';
+import useAutoFocusInput from '@hooks/useAutoFocusInput';
+import useLocalize from '@hooks/useLocalize';
+import useThemeStyles from '@hooks/useThemeStyles';
+import Navigation from '@libs/Navigation/Navigation';
+import type {PlatformStackScreenProps} from '@libs/Navigation/PlatformStackNavigation/types';
+import {getPerDiemCustomUnit} from '@libs/PolicyUtils';
+import type {SettingsNavigatorParamList} from '@navigation/types';
+import AccessOrNotFoundWrapper from '@pages/workspace/AccessOrNotFoundWrapper';
+import * as PerDiem from '@userActions/Policy/PerDiem';
+import CONST from '@src/CONST';
+import ONYXKEYS from '@src/ONYXKEYS';
+import ROUTES from '@src/ROUTES';
+import type SCREENS from '@src/SCREENS';
+import INPUT_IDS from '@src/types/form/WorkspacePerDiemForm';
+import {isEmptyObject} from '@src/types/utils/EmptyObject';
+
+type EditPerDiemSubratePageProps = PlatformStackScreenProps;
+
+function EditPerDiemSubratePage({route}: EditPerDiemSubratePageProps) {
+ const styles = useThemeStyles();
+ const {translate} = useLocalize();
+ const policyID = route.params.policyID;
+ const rateID = route.params.rateID;
+ const subRateID = route.params.subRateID;
+ const [policy] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY}${policyID}`);
+
+ const customUnit = getPerDiemCustomUnit(policy);
+
+ const selectedRate = customUnit?.rates?.[rateID];
+ const selectedSubrate = selectedRate?.subRates?.find((subRate) => subRate.id === subRateID);
+
+ const {inputCallbackRef} = useAutoFocusInput();
+
+ const validate = useCallback(
+ (values: FormOnyxValues): FormInputErrors => {
+ const errors: FormInputErrors = {};
+
+ if (!values.subrate.trim()) {
+ errors.subrate = translate('common.error.fieldRequired');
+ }
+
+ return errors;
+ },
+ [translate],
+ );
+
+ const editSubrate = useCallback(
+ (values: FormOnyxValues) => {
+ const newSubrate = values.subrate.trim();
+ if (newSubrate !== selectedSubrate?.name) {
+ PerDiem.editPerDiemRateSubrate(policyID, rateID, subRateID, customUnit, newSubrate);
+ }
+ Navigation.goBack(ROUTES.WORKSPACE_PER_DIEM_DETAILS.getRoute(policyID, rateID, subRateID));
+ },
+ [selectedSubrate?.name, policyID, rateID, subRateID, customUnit],
+ );
+
+ return (
+
+
+ Navigation.goBack(ROUTES.WORKSPACE_PER_DIEM_DETAILS.getRoute(policyID, rateID, subRateID))}
+ />
+
+
+
+
+
+ );
+}
+
+EditPerDiemSubratePage.displayName = 'EditPerDiemSubratePage';
+
+export default EditPerDiemSubratePage;
diff --git a/src/pages/workspace/perDiem/WorkspacePerDiemDetailsPage.tsx b/src/pages/workspace/perDiem/WorkspacePerDiemDetailsPage.tsx
new file mode 100644
index 000000000000..d1dea9c99f77
--- /dev/null
+++ b/src/pages/workspace/perDiem/WorkspacePerDiemDetailsPage.tsx
@@ -0,0 +1,128 @@
+import React, {useState} from 'react';
+import {View} from 'react-native';
+import {useOnyx} from 'react-native-onyx';
+import FullPageOfflineBlockingView from '@components/BlockingViews/FullPageOfflineBlockingView';
+import ConfirmModal from '@components/ConfirmModal';
+import HeaderWithBackButton from '@components/HeaderWithBackButton';
+import * as Expensicons from '@components/Icon/Expensicons';
+import MenuItem from '@components/MenuItem';
+import MenuItemWithTopDescription from '@components/MenuItemWithTopDescription';
+import ScreenWrapper from '@components/ScreenWrapper';
+import ScrollView from '@components/ScrollView';
+import useLocalize from '@hooks/useLocalize';
+import useThemeStyles from '@hooks/useThemeStyles';
+import {convertToFrontendAmountAsString, getCurrencySymbol} from '@libs/CurrencyUtils';
+import Navigation from '@libs/Navigation/Navigation';
+import type {PlatformStackScreenProps} from '@libs/Navigation/PlatformStackNavigation/types';
+import {getPerDiemCustomUnit} from '@libs/PolicyUtils';
+import type {SettingsNavigatorParamList} from '@navigation/types';
+import AccessOrNotFoundWrapper from '@pages/workspace/AccessOrNotFoundWrapper';
+import * as PerDiem from '@userActions/Policy/PerDiem';
+import CONST from '@src/CONST';
+import ONYXKEYS from '@src/ONYXKEYS';
+import ROUTES from '@src/ROUTES';
+import type SCREENS from '@src/SCREENS';
+import {isEmptyObject} from '@src/types/utils/EmptyObject';
+
+type WorkspacePerDiemDetailsPageProps = PlatformStackScreenProps;
+
+function WorkspacePerDiemDetailsPage({route}: WorkspacePerDiemDetailsPageProps) {
+ const policyID = route.params.policyID;
+ const rateID = route.params.rateID;
+ const subRateID = route.params.subRateID;
+ const [deletePerDiemConfirmModalVisible, setDeletePerDiemConfirmModalVisible] = useState(false);
+ const [policy] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY}${policyID}`);
+
+ const styles = useThemeStyles();
+ const {translate} = useLocalize();
+ const customUnit = getPerDiemCustomUnit(policy);
+
+ const selectedRate = customUnit?.rates?.[rateID];
+ const selectedSubRate = selectedRate?.subRates?.find((subRate) => subRate.id === subRateID);
+
+ const amountValue = selectedSubRate?.rate ? convertToFrontendAmountAsString(Number(selectedSubRate.rate)) : undefined;
+ const currencyValue = selectedRate?.currency ? `${selectedRate.currency} - ${getCurrencySymbol(selectedRate.currency)}` : undefined;
+
+ const FullPageBlockingView = isEmptyObject(selectedSubRate) ? FullPageOfflineBlockingView : View;
+
+ const handleDeletePerDiemRate = () => {
+ PerDiem.deleteWorkspacePerDiemRates(policyID, customUnit, [
+ {
+ destination: selectedRate?.name ?? '',
+ subRateName: selectedSubRate?.name ?? '',
+ rate: selectedSubRate?.rate ?? 0,
+ currency: selectedRate?.currency ?? '',
+ rateID,
+ subRateID,
+ },
+ ]);
+ setDeletePerDiemConfirmModalVisible(false);
+ Navigation.goBack();
+ };
+
+ return (
+
+
+
+ setDeletePerDiemConfirmModalVisible(false)}
+ title={translate('workspace.perDiem.deletePerDiemRate')}
+ prompt={translate('workspace.perDiem.areYouSureDelete', {count: 1})}
+ confirmText={translate('common.delete')}
+ cancelText={translate('common.cancel')}
+ danger
+ />
+
+
+ Navigation.navigate(ROUTES.WORKSPACE_PER_DIEM_EDIT_DESTINATION.getRoute(policyID, rateID, subRateID))}
+ shouldShowRightIcon
+ />
+ Navigation.navigate(ROUTES.WORKSPACE_PER_DIEM_EDIT_SUBRATE.getRoute(policyID, rateID, subRateID))}
+ shouldShowRightIcon
+ />
+ Navigation.navigate(ROUTES.WORKSPACE_PER_DIEM_EDIT_AMOUNT.getRoute(policyID, rateID, subRateID))}
+ shouldShowRightIcon
+ />
+ Navigation.navigate(ROUTES.WORKSPACE_PER_DIEM_EDIT_CURRENCY.getRoute(policyID, rateID, subRateID))}
+ shouldShowRightIcon
+ />
+ setDeletePerDiemConfirmModalVisible(true)}
+ />
+
+
+
+
+ );
+}
+
+WorkspacePerDiemDetailsPage.displayName = 'WorkspacePerDiemDetailsPage';
+
+export default WorkspacePerDiemDetailsPage;
diff --git a/src/pages/workspace/perDiem/WorkspacePerDiemPage.tsx b/src/pages/workspace/perDiem/WorkspacePerDiemPage.tsx
index 0753ba4772f6..894dc3307826 100644
--- a/src/pages/workspace/perDiem/WorkspacePerDiemPage.tsx
+++ b/src/pages/workspace/perDiem/WorkspacePerDiemPage.tsx
@@ -76,7 +76,7 @@ function getSubRatesData(customUnitRates: Rate[]) {
subRateName: subRate.name,
rate: subRate.rate,
currency: rate.currency ?? CONST.CURRENCY.USD,
- rateID: rate.customUnitRateID ?? '',
+ rateID: rate.customUnitRateID,
subRateID: subRate.id,
});
}
@@ -100,7 +100,7 @@ function generateSingleSubRateData(customUnitRates: Rate[], rateID: string, subR
subRateName: selectedSubRate.name,
rate: selectedSubRate.rate,
currency: selectedRate.currency ?? CONST.CURRENCY.USD,
- rateID: selectedRate.customUnitRateID ?? '',
+ rateID: selectedRate.customUnitRateID,
subRateID: selectedSubRate.id,
};
}
@@ -120,7 +120,7 @@ function WorkspacePerDiemPage({route}: WorkspacePerDiemPageProps) {
const [deletePerDiemConfirmModalVisible, setDeletePerDiemConfirmModalVisible] = useState(false);
const [isDownloadFailureModalVisible, setIsDownloadFailureModalVisible] = useState(false);
const isFocused = useIsFocused();
- const policyID = route.params.policyID ?? '-1';
+ const policyID = route.params.policyID;
const backTo = route.params?.backTo;
const policy = usePolicy(policyID);
const {selectionMode} = useMobileSelectionMode();
@@ -223,18 +223,12 @@ function WorkspacePerDiemPage({route}: WorkspacePerDiemPageProps) {
Navigation.navigate(ROUTES.WORKSPACE_PER_DIEM_SETTINGS.getRoute(policyID));
};
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
const openSubRateDetails = (rate: PolicyOption) => {
- // TODO: Uncomment this when the import feature is ready
- // Navigation.navigate(ROUTES.WORKSPACE_PER_DIEM_RATE_DETAILS.getRoute(policyID, rate.rateID, rate.subRateID));
- };
-
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
- const dismissError = (item: PolicyOption) => {
- // TODO: Implement this when the editing feature is ready
+ Navigation.navigate(ROUTES.WORKSPACE_PER_DIEM_DETAILS.getRoute(policyID, rate.rateID, rate.subRateID));
};
const handleDeletePerDiemRates = () => {
+ PerDiem.deleteWorkspacePerDiemRates(policyID, customUnit, selectedPerDiem);
setSelectedPerDiem([]);
setDeletePerDiemConfirmModalVisible(false);
};
@@ -424,7 +418,6 @@ function WorkspacePerDiemPage({route}: WorkspacePerDiemPageProps) {
shouldPreventDefaultFocusOnSelectRow={!DeviceCapabilities.canUseTouchScreen()}
onSelectAll={toggleAllSubRates}
ListItem={TableListItem}
- onDismissError={dismissError}
customListHeader={getCustomListHeader()}
listHeaderWrapperStyle={[styles.ph9, styles.pv3, styles.pb5]}
listHeaderContent={shouldUseNarrowLayout ? getHeaderText() : null}
diff --git a/src/pages/workspace/tags/WorkspaceTagsPage.tsx b/src/pages/workspace/tags/WorkspaceTagsPage.tsx
index 61bd2e3aa42f..b86a35fa6fca 100644
--- a/src/pages/workspace/tags/WorkspaceTagsPage.tsx
+++ b/src/pages/workspace/tags/WorkspaceTagsPage.tsx
@@ -1,6 +1,6 @@
-import {useFocusEffect, useIsFocused} from '@react-navigation/native';
+import {useFocusEffect} from '@react-navigation/native';
import lodashSortBy from 'lodash/sortBy';
-import React, {useCallback, useEffect, useMemo, useState} from 'react';
+import React, {useCallback, useMemo, useState} from 'react';
import {ActivityIndicator, View} from 'react-native';
import {useOnyx} from 'react-native-onyx';
import Button from '@components/Button';
@@ -22,6 +22,7 @@ import CustomListHeader from '@components/SelectionListWithModal/CustomListHeade
import TableListItemSkeleton from '@components/Skeletons/TableRowSkeleton';
import Text from '@components/Text';
import TextLink from '@components/TextLink';
+import useCleanupSelectedOptions from '@hooks/useCleanupSelectedOptions';
import useEnvironment from '@hooks/useEnvironment';
import useLocalize from '@hooks/useLocalize';
import useMobileSelectionMode from '@hooks/useMobileSelectionMode';
@@ -64,7 +65,6 @@ function WorkspaceTagsPage({route}: WorkspaceTagsPageProps) {
const [isDownloadFailureModalVisible, setIsDownloadFailureModalVisible] = useState(false);
const [isDeleteTagsConfirmModalVisible, setIsDeleteTagsConfirmModalVisible] = useState(false);
const [isOfflineModalVisible, setIsOfflineModalVisible] = useState(false);
- const isFocused = useIsFocused();
const policyID = route.params.policyID ?? '-1';
const backTo = route.params.backTo;
const policy = usePolicy(policyID);
@@ -87,12 +87,8 @@ function WorkspaceTagsPage({route}: WorkspaceTagsPageProps) {
useFocusEffect(fetchTags);
- useEffect(() => {
- if (isFocused) {
- return;
- }
- setSelectedTags({});
- }, [isFocused]);
+ const cleanupSelectedOption = useCallback(() => setSelectedTags({}), []);
+ useCleanupSelectedOptions(cleanupSelectedOption);
const getPendingAction = (policyTagList: PolicyTagList): PendingAction | undefined => {
if (!policyTagList) {
@@ -176,6 +172,10 @@ function WorkspaceTagsPage({route}: WorkspaceTagsPageProps) {
};
const navigateToTagSettings = (tag: TagListItem) => {
+ if (isSmallScreenWidth && selectionMode?.isEnabled) {
+ toggleTag(tag);
+ return;
+ }
if (tag.orderWeight !== undefined) {
Navigation.navigate(
isQuickSettingsFlow ? ROUTES.SETTINGS_TAG_LIST_VIEW.getRoute(policyID, tag.orderWeight, backTo) : ROUTES.WORKSPACE_TAG_LIST_VIEW.getRoute(policyID, tag.orderWeight),
diff --git a/src/pages/workspace/taxes/WorkspaceTaxesPage.tsx b/src/pages/workspace/taxes/WorkspaceTaxesPage.tsx
index e064c04878a1..e588a1ecb313 100644
--- a/src/pages/workspace/taxes/WorkspaceTaxesPage.tsx
+++ b/src/pages/workspace/taxes/WorkspaceTaxesPage.tsx
@@ -1,5 +1,5 @@
-import {useFocusEffect, useIsFocused} from '@react-navigation/native';
-import React, {useCallback, useEffect, useMemo, useState} from 'react';
+import {useFocusEffect} from '@react-navigation/native';
+import React, {useCallback, useMemo, useState} from 'react';
import {ActivityIndicator, View} from 'react-native';
import {useOnyx} from 'react-native-onyx';
import Button from '@components/Button';
@@ -17,6 +17,7 @@ import SelectionListWithModal from '@components/SelectionListWithModal';
import CustomListHeader from '@components/SelectionListWithModal/CustomListHeader';
import Text from '@components/Text';
import TextLink from '@components/TextLink';
+import useCleanupSelectedOptions from '@hooks/useCleanupSelectedOptions';
import useEnvironment from '@hooks/useEnvironment';
import useLocalize from '@hooks/useLocalize';
import useMobileSelectionMode from '@hooks/useMobileSelectionMode';
@@ -51,7 +52,8 @@ function WorkspaceTaxesPage({
params: {policyID},
},
}: WorkspaceTaxesPageProps) {
- const {shouldUseNarrowLayout} = useResponsiveLayout();
+ // eslint-disable-next-line rulesdir/prefer-shouldUseNarrowLayout-instead-of-isSmallScreenWidth
+ const {shouldUseNarrowLayout, isSmallScreenWidth} = useResponsiveLayout();
const styles = useThemeStyles();
const theme = useTheme();
const {translate} = useLocalize();
@@ -61,7 +63,6 @@ function WorkspaceTaxesPage({
const {selectionMode} = useMobileSelectionMode();
const defaultExternalID = policy?.taxRates?.defaultExternalID;
const foreignTaxDefault = policy?.taxRates?.foreignTaxDefault;
- const isFocused = useIsFocused();
const hasAccountingConnections = PolicyUtils.hasAccountingConnections(policy);
const [connectionSyncProgress] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY_CONNECTION_SYNC_PROGRESS}${policy?.id}`);
const isSyncInProgress = isConnectionInProgress(connectionSyncProgress, policy);
@@ -86,12 +87,8 @@ function WorkspaceTaxesPage({
}, [fetchTaxes]),
);
- useEffect(() => {
- if (isFocused) {
- return;
- }
- setSelectedTaxesIDs([]);
- }, [isFocused]);
+ const cleanupSelectedOption = useCallback(() => setSelectedTaxesIDs([]), []);
+ useCleanupSelectedOptions(cleanupSelectedOption);
const textForDefault = useCallback(
(taxID: string, taxRate: TaxRate): string => {
@@ -192,6 +189,10 @@ function WorkspaceTaxesPage({
if (!taxRate.keyForList) {
return;
}
+ if (isSmallScreenWidth && selectionMode?.isEnabled) {
+ toggleTax(taxRate);
+ return;
+ }
Navigation.navigate(ROUTES.WORKSPACE_TAX_EDIT.getRoute(policyID, taxRate.keyForList));
};
diff --git a/src/types/form/WorkspacePerDiemForm.ts b/src/types/form/WorkspacePerDiemForm.ts
new file mode 100644
index 000000000000..86dc58cb1d5c
--- /dev/null
+++ b/src/types/form/WorkspacePerDiemForm.ts
@@ -0,0 +1,22 @@
+import type {ValueOf} from 'type-fest';
+import type Form from './Form';
+
+const INPUT_IDS = {
+ DESTINATION: 'destination',
+ SUBRATE: 'subrate',
+ AMOUNT: 'amount',
+} as const;
+
+type InputID = ValueOf;
+
+type WorkspacePerDiemForm = Form<
+ InputID,
+ {
+ [INPUT_IDS.DESTINATION]: string;
+ [INPUT_IDS.SUBRATE]: string;
+ [INPUT_IDS.AMOUNT]: string;
+ }
+>;
+
+export type {WorkspacePerDiemForm};
+export default INPUT_IDS;
diff --git a/src/types/form/index.ts b/src/types/form/index.ts
index e8e37bebef9a..3c9d90ba3c2d 100644
--- a/src/types/form/index.ts
+++ b/src/types/form/index.ts
@@ -87,3 +87,4 @@ export type {WorkspaceCompanyCardFeedName} from './WorkspaceCompanyCardFeedName'
export type {SearchSavedSearchRenameForm} from './SearchSavedSearchRenameForm';
export type {WorkspaceCompanyCardEditName} from './WorkspaceCompanyCardEditName';
export type {PersonalDetailsForm} from './PersonalDetailsForm';
+export type {WorkspacePerDiemForm} from './WorkspacePerDiemForm';
diff --git a/src/types/modules/pdf.worker.d.ts b/src/types/modules/pdf.worker.d.ts
index a6d70e529b7f..c636372b411d 100644
--- a/src/types/modules/pdf.worker.d.ts
+++ b/src/types/modules/pdf.worker.d.ts
@@ -1 +1 @@
-declare module 'pdfjs-dist/legacy/build/pdf.worker.min.mjs';
+declare module 'pdfjs-dist/build/pdf.worker.min.mjs';
diff --git a/src/types/onyx/Account.ts b/src/types/onyx/Account.ts
index 90f8b9b8e2c2..33a593ca2b10 100644
--- a/src/types/onyx/Account.ts
+++ b/src/types/onyx/Account.ts
@@ -67,12 +67,6 @@ type SMSDeliveryFailureStatus = {
/** The message associated with the SMS delivery failure */
message: string;
-
- /** Indicates whether the SMS delivery failure status has been reset by an API call */
- isReset?: boolean;
-
- /** Whether a sign is loading */
- isLoading?: boolean;
};
/** Model of user account */
diff --git a/src/types/onyx/Policy.ts b/src/types/onyx/Policy.ts
index 9883b23067fe..892cdd527ff2 100644
--- a/src/types/onyx/Policy.ts
+++ b/src/types/onyx/Policy.ts
@@ -43,7 +43,7 @@ type Rate = OnyxCommon.OnyxValueWithOfflineFeedback<
currency?: string;
/** Generated ID to identify the rate */
- customUnitRateID?: string;
+ customUnitRateID: string;
/** Whether this rate is currently enabled */
enabled?: boolean;
diff --git a/tests/actions/IOUTest.ts b/tests/actions/IOUTest.ts
index 2127b4b819ca..05a6612e4546 100644
--- a/tests/actions/IOUTest.ts
+++ b/tests/actions/IOUTest.ts
@@ -21,6 +21,7 @@ import type * as OnyxTypes from '@src/types/onyx';
import type {Participant} from '@src/types/onyx/Report';
import {toCollectionDataSet} from '@src/types/utils/CollectionDataSet';
import {isEmptyObject} from '@src/types/utils/EmptyObject';
+import createRandomPolicy, {createCategoryTaxExpenseRules} from '../utils/collections/policies';
import createRandomTransaction from '../utils/collections/transaction';
import PusherHelper from '../utils/PusherHelper';
import type {MockFetch} from '../utils/TestHelper';
@@ -2971,6 +2972,7 @@ describe('actions/IOU', () => {
expect(iouReport).toHaveProperty('chatReportID');
// Then we expect to navigate to the iou report
+
expect(IOU_REPORT_ID).not.toBeUndefined();
if (IOU_REPORT_ID) {
expect(navigateToAfterDelete).toEqual(ROUTES.REPORT_WITH_ID.getRoute(IOU_REPORT_ID));
@@ -3350,4 +3352,296 @@ describe('actions/IOU', () => {
});
});
});
+
+ describe('setMoneyRequestCategory', () => {
+ it('should set the associated tax for the category based on the tax expense rules', async () => {
+ // Given a policy with tax expense rules associated with category
+ const transactionID = '1';
+ const category = 'Advertising';
+ const policyID = '2';
+ const taxCode = 'id_TAX_EXEMPT';
+ const ruleTaxCode = 'id_TAX_RATE_1';
+ const fakePolicy: OnyxTypes.Policy = {
+ ...createRandomPolicy(Number(policyID)),
+ taxRates: CONST.DEFAULT_TAX,
+ rules: {expenseRules: createCategoryTaxExpenseRules(category, ruleTaxCode)},
+ };
+ await Onyx.merge(`${ONYXKEYS.COLLECTION.TRANSACTION_DRAFT}${transactionID}`, {
+ taxCode,
+ taxAmount: 0,
+ amount: 100,
+ });
+ await Onyx.merge(`${ONYXKEYS.COLLECTION.POLICY}${policyID}`, fakePolicy);
+
+ // When setting the money request category
+ IOU.setMoneyRequestCategory(transactionID, category, policyID);
+
+ await waitForBatchedUpdates();
+
+ // Then the transaction tax rate and amount should be updated based on the expense rules
+ await new Promise((resolve) => {
+ const connection = Onyx.connect({
+ key: `${ONYXKEYS.COLLECTION.TRANSACTION_DRAFT}${transactionID}`,
+ callback: (transaction) => {
+ Onyx.disconnect(connection);
+ expect(transaction?.taxCode).toBe(ruleTaxCode);
+ expect(transaction?.taxAmount).toBe(5);
+ resolve();
+ },
+ });
+ });
+ });
+
+ it('should not change the tax if there are no tax expense rules', async () => {
+ // Given a policy without tax expense rules
+ const transactionID = '1';
+ const category = 'Advertising';
+ const policyID = '2';
+ const taxCode = 'id_TAX_EXEMPT';
+ const taxAmount = 0;
+ const fakePolicy: OnyxTypes.Policy = {
+ ...createRandomPolicy(Number(policyID)),
+ taxRates: CONST.DEFAULT_TAX,
+ rules: {},
+ };
+ await Onyx.merge(`${ONYXKEYS.COLLECTION.TRANSACTION_DRAFT}${transactionID}`, {
+ taxCode,
+ taxAmount,
+ amount: 100,
+ });
+ await Onyx.merge(`${ONYXKEYS.COLLECTION.POLICY}${policyID}`, fakePolicy);
+
+ // When setting the money request category
+ IOU.setMoneyRequestCategory(transactionID, category, policyID);
+
+ await waitForBatchedUpdates();
+
+ // Then the transaction tax rate and amount shouldn't be updated
+ await new Promise((resolve) => {
+ const connection = Onyx.connect({
+ key: `${ONYXKEYS.COLLECTION.TRANSACTION_DRAFT}${transactionID}`,
+ callback: (transaction) => {
+ Onyx.disconnect(connection);
+ expect(transaction?.taxCode).toBe(taxCode);
+ expect(transaction?.taxAmount).toBe(taxAmount);
+ resolve();
+ },
+ });
+ });
+ });
+
+ it('should clear the tax when the policyID is empty', async () => {
+ // Given a transaction with a tax
+ const transactionID = '1';
+ const taxCode = 'id_TAX_EXEMPT';
+ const taxAmount = 0;
+ await Onyx.merge(`${ONYXKEYS.COLLECTION.TRANSACTION_DRAFT}${transactionID}`, {
+ taxCode,
+ taxAmount,
+ amount: 100,
+ });
+
+ // When setting the money request category without a policyID
+ IOU.setMoneyRequestCategory(transactionID, '');
+ await waitForBatchedUpdates();
+
+ // Then the transaction tax should be cleared
+ await new Promise((resolve) => {
+ const connection = Onyx.connect({
+ key: `${ONYXKEYS.COLLECTION.TRANSACTION_DRAFT}${transactionID}`,
+ callback: (transaction) => {
+ Onyx.disconnect(connection);
+ expect(transaction?.taxCode).toBe('');
+ expect(transaction?.taxAmount).toBeUndefined();
+ resolve();
+ },
+ });
+ });
+ });
+ });
+
+ describe('updateMoneyRequestCategory', () => {
+ it('should update the tax when there are tax expense rules', async () => {
+ // Given a policy with tax expense rules associated with category
+ const transactionID = '1';
+ const policyID = '2';
+ const category = 'Advertising';
+ const taxCode = 'id_TAX_EXEMPT';
+ const ruleTaxCode = 'id_TAX_RATE_1';
+ const fakePolicy: OnyxTypes.Policy = {
+ ...createRandomPolicy(Number(policyID)),
+ taxRates: CONST.DEFAULT_TAX,
+ rules: {expenseRules: createCategoryTaxExpenseRules(category, ruleTaxCode)},
+ };
+ await Onyx.merge(`${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`, {
+ taxCode,
+ taxAmount: 0,
+ amount: 100,
+ });
+ await Onyx.merge(`${ONYXKEYS.COLLECTION.POLICY}${policyID}`, fakePolicy);
+
+ // When updating a money request category
+ IOU.updateMoneyRequestCategory(transactionID, '3', category, fakePolicy, undefined, undefined);
+
+ await waitForBatchedUpdates();
+
+ // Then the transaction tax rate and amount should be updated based on the expense rules
+ await new Promise((resolve) => {
+ const connection = Onyx.connect({
+ key: `${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`,
+ callback: (transaction) => {
+ Onyx.disconnect(connection);
+ expect(transaction?.taxCode).toBe(ruleTaxCode);
+ expect(transaction?.taxAmount).toBe(5);
+ resolve();
+ },
+ });
+ });
+ });
+
+ it('should not update the tax when there are no tax expense rules', async () => {
+ // Given a policy without tax expense rules
+ const transactionID = '1';
+ const policyID = '2';
+ const category = 'Advertising';
+ const fakePolicy: OnyxTypes.Policy = {
+ ...createRandomPolicy(Number(policyID)),
+ taxRates: CONST.DEFAULT_TAX,
+ rules: {},
+ };
+ await Onyx.merge(`${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`, {amount: 100});
+ await Onyx.merge(`${ONYXKEYS.COLLECTION.POLICY}${policyID}`, fakePolicy);
+
+ // When updating the money request category
+ IOU.updateMoneyRequestCategory(transactionID, '3', category, fakePolicy, undefined, undefined);
+
+ await waitForBatchedUpdates();
+
+ // Then the transaction tax rate and amount shouldn't be updated
+ await new Promise((resolve) => {
+ const connection = Onyx.connect({
+ key: `${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`,
+ callback: (transaction) => {
+ Onyx.disconnect(connection);
+ expect(transaction?.taxCode).toBeUndefined();
+ expect(transaction?.taxAmount).toBeUndefined();
+ resolve();
+ },
+ });
+ });
+ });
+ });
+
+ describe('setDraftSplitTransaction', () => {
+ it('should set the associated tax for the category based on the tax expense rules', async () => {
+ // Given a policy with tax expense rules associated with category
+ const transactionID = '1';
+ const category = 'Advertising';
+ const policyID = '2';
+ const taxCode = 'id_TAX_EXEMPT';
+ const ruleTaxCode = 'id_TAX_RATE_1';
+ const fakePolicy: OnyxTypes.Policy = {
+ ...createRandomPolicy(Number(policyID)),
+ taxRates: CONST.DEFAULT_TAX,
+ rules: {expenseRules: createCategoryTaxExpenseRules(category, ruleTaxCode)},
+ };
+ await Onyx.merge(`${ONYXKEYS.COLLECTION.SPLIT_TRANSACTION_DRAFT}${transactionID}`, {
+ taxCode,
+ taxAmount: 0,
+ amount: 100,
+ });
+ await Onyx.merge(`${ONYXKEYS.COLLECTION.POLICY}${policyID}`, fakePolicy);
+
+ // When setting a category of a draft split transaction
+ IOU.setDraftSplitTransaction(transactionID, {category}, fakePolicy);
+
+ await waitForBatchedUpdates();
+
+ // Then the transaction tax rate and amount should be updated based on the expense rules
+ await new Promise((resolve) => {
+ const connection = Onyx.connect({
+ key: `${ONYXKEYS.COLLECTION.SPLIT_TRANSACTION_DRAFT}${transactionID}`,
+ callback: (transaction) => {
+ Onyx.disconnect(connection);
+ expect(transaction?.taxCode).toBe(ruleTaxCode);
+ expect(transaction?.taxAmount).toBe(5);
+ resolve();
+ },
+ });
+ });
+ });
+
+ describe('should not change the tax', () => {
+ it('if there is no tax expense rules', async () => {
+ // Given a policy without tax expense rules
+ const transactionID = '1';
+ const category = 'Advertising';
+ const policyID = '2';
+ const taxCode = 'id_TAX_EXEMPT';
+ const taxAmount = 0;
+ const fakePolicy: OnyxTypes.Policy = {
+ ...createRandomPolicy(Number(policyID)),
+ taxRates: CONST.DEFAULT_TAX,
+ rules: {},
+ };
+ await Onyx.merge(`${ONYXKEYS.COLLECTION.SPLIT_TRANSACTION_DRAFT}${transactionID}`, {
+ taxCode,
+ taxAmount,
+ amount: 100,
+ });
+ await Onyx.merge(`${ONYXKEYS.COLLECTION.POLICY}${policyID}`, fakePolicy);
+
+ // When setting a category of a draft split transaction
+ IOU.setDraftSplitTransaction(transactionID, {category}, fakePolicy);
+
+ await waitForBatchedUpdates();
+
+ // Then the transaction tax rate and amount shouldn't be updated
+ await new Promise((resolve) => {
+ const connection = Onyx.connect({
+ key: `${ONYXKEYS.COLLECTION.SPLIT_TRANSACTION_DRAFT}${transactionID}`,
+ callback: (transaction) => {
+ Onyx.disconnect(connection);
+ expect(transaction?.taxCode).toBe(taxCode);
+ expect(transaction?.taxAmount).toBe(taxAmount);
+ resolve();
+ },
+ });
+ });
+ });
+
+ it('if we are not updating category', async () => {
+ // Given a policy with tax expense rules associated with category
+ const transactionID = '1';
+ const category = 'Advertising';
+ const policyID = '2';
+ const ruleTaxCode = 'id_TAX_RATE_1';
+ const fakePolicy: OnyxTypes.Policy = {
+ ...createRandomPolicy(Number(policyID)),
+ taxRates: CONST.DEFAULT_TAX,
+ rules: {expenseRules: createCategoryTaxExpenseRules(category, ruleTaxCode)},
+ };
+ await Onyx.merge(`${ONYXKEYS.COLLECTION.SPLIT_TRANSACTION_DRAFT}${transactionID}`, {amount: 100});
+ await Onyx.merge(`${ONYXKEYS.COLLECTION.POLICY}${policyID}`, fakePolicy);
+
+ // When setting a draft split transaction without category update
+ IOU.setDraftSplitTransaction(transactionID, {}, fakePolicy);
+
+ await waitForBatchedUpdates();
+
+ // Then the transaction tax rate and amount shouldn't be updated
+ await new Promise((resolve) => {
+ const connection = Onyx.connect({
+ key: `${ONYXKEYS.COLLECTION.SPLIT_TRANSACTION_DRAFT}${transactionID}`,
+ callback: (transaction) => {
+ Onyx.disconnect(connection);
+ expect(transaction?.taxCode).toBeUndefined();
+ expect(transaction?.taxAmount).toBeUndefined();
+ resolve();
+ },
+ });
+ });
+ });
+ });
+ });
});
diff --git a/tests/unit/TransactionUtilsTest.ts b/tests/unit/TransactionUtilsTest.ts
index 6cf3704aeb09..a6c847e5f7f4 100644
--- a/tests/unit/TransactionUtilsTest.ts
+++ b/tests/unit/TransactionUtilsTest.ts
@@ -1,6 +1,8 @@
+import CONST from '@src/CONST';
import type {Attendee} from '@src/types/onyx/IOU';
import * as TransactionUtils from '../../src/libs/TransactionUtils';
-import type {Transaction} from '../../src/types/onyx';
+import type {Policy, Transaction} from '../../src/types/onyx';
+import createRandomPolicy, {createCategoryTaxExpenseRules} from '../utils/collections/policies';
function generateTransaction(values: Partial = {}): Transaction {
const reportID = '1';
@@ -92,4 +94,61 @@ describe('TransactionUtils', () => {
});
});
});
+
+ describe('getCategoryTaxCodeAndAmount', () => {
+ it('should return the associated tax when the category matches the tax expense rules', () => {
+ // Given a policy with tax expense rules associated with a category
+ const category = 'Advertising';
+ const fakePolicy: Policy = {
+ ...createRandomPolicy(0),
+ taxRates: CONST.DEFAULT_TAX,
+ rules: {expenseRules: createCategoryTaxExpenseRules(category, 'id_TAX_RATE_1')},
+ };
+
+ // When retrieving the tax from the associated category
+ const transaction = generateTransaction();
+ const {categoryTaxCode, categoryTaxAmount} = TransactionUtils.getCategoryTaxCodeAndAmount(category, transaction, fakePolicy);
+
+ // Then it should return the associated tax code and amount
+ expect(categoryTaxCode).toBe('id_TAX_RATE_1');
+ expect(categoryTaxAmount).toBe(5);
+ });
+
+ it("should return the default tax when the category doesn't match the tax expense rules", () => {
+ // Given a policy with tax expense rules associated with a category
+ const ruleCategory = 'Advertising';
+ const selectedCategory = 'Benefits';
+ const fakePolicy: Policy = {
+ ...createRandomPolicy(0),
+ taxRates: CONST.DEFAULT_TAX,
+ rules: {expenseRules: createCategoryTaxExpenseRules(ruleCategory, 'id_TAX_RATE_1')},
+ };
+
+ // When retrieving the tax from a category that is not associated with the tax expense rules
+ const transaction = generateTransaction();
+ const {categoryTaxCode, categoryTaxAmount} = TransactionUtils.getCategoryTaxCodeAndAmount(selectedCategory, transaction, fakePolicy);
+
+ // Then it should return the default tax code and amount
+ expect(categoryTaxCode).toBe('id_TAX_EXEMPT');
+ expect(categoryTaxAmount).toBe(0);
+ });
+
+ it('should return and undefined tax when there are no policy tax expense rules', () => {
+ // Given a policy without tax expense rules
+ const category = 'Advertising';
+ const fakePolicy: Policy = {
+ ...createRandomPolicy(0),
+ taxRates: CONST.DEFAULT_TAX,
+ rules: {},
+ };
+
+ // When retrieving the tax from a category
+ const transaction = generateTransaction();
+ const {categoryTaxCode, categoryTaxAmount} = TransactionUtils.getCategoryTaxCodeAndAmount(category, transaction, fakePolicy);
+
+ // Then it should return undefined for both the tax code and the tax amount
+ expect(categoryTaxCode).toBe(undefined);
+ expect(categoryTaxAmount).toBe(undefined);
+ });
+ });
});
diff --git a/tests/utils/collections/policies.ts b/tests/utils/collections/policies.ts
index bda1c3242997..ca26774692a0 100644
--- a/tests/utils/collections/policies.ts
+++ b/tests/utils/collections/policies.ts
@@ -35,3 +35,20 @@ export default function createRandomPolicy(index: number, type?: ValueOf