diff --git a/.eslintrc.js b/.eslintrc.js
index cfbfdcc8fe91..fefad92ce29d 100644
--- a/.eslintrc.js
+++ b/.eslintrc.js
@@ -294,6 +294,7 @@ module.exports = {
files: ['*.ts', '*.tsx'],
rules: {
'rulesdir/prefer-at': 'error',
+ 'rulesdir/boolean-conditional-rendering': 'error',
},
},
],
diff --git a/.github/ISSUE_TEMPLATE/Standard.md b/.github/ISSUE_TEMPLATE/Standard.md
index fa50d48b341b..7ae439777e78 100644
--- a/.github/ISSUE_TEMPLATE/Standard.md
+++ b/.github/ISSUE_TEMPLATE/Standard.md
@@ -16,7 +16,7 @@ ___
**Logs:** https://stackoverflow.com/c/expensify/questions/4856
**Expensify/Expensify Issue URL:**
**Issue reported by:**
-**Slack conversation:**
+**Slack conversation** (hyperlinked to channel name):
## Action Performed:
Break down in numbered steps
diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md
index 36b921570e7f..c1238d6805aa 100644
--- a/.github/PULL_REQUEST_TEMPLATE.md
+++ b/.github/PULL_REQUEST_TEMPLATE.md
@@ -1,7 +1,7 @@
-### Details
-
+### Explanation of Change
+
### Fixed Issues
-
-
-
-
-
-
\ No newline at end of file
+
\ No newline at end of file
diff --git a/assets/images/bookmark.svg b/assets/images/bookmark.svg
index d7c1a8397b37..7e1cb61e40bf 100644
--- a/assets/images/bookmark.svg
+++ b/assets/images/bookmark.svg
@@ -1 +1 @@
-
+
\ No newline at end of file
diff --git a/assets/images/caret-up-down.svg b/assets/images/caret-up-down.svg
index d08aa2a1ebbd..054aa53e8f75 100644
--- a/assets/images/caret-up-down.svg
+++ b/assets/images/caret-up-down.svg
@@ -1,17 +1 @@
-
-
\ No newline at end of file
+
\ No newline at end of file
diff --git a/assets/images/companyCards/amex.svg b/assets/images/companyCards/amex.svg
index 73e8164cdc63..61a7561a0622 100644
--- a/assets/images/companyCards/amex.svg
+++ b/assets/images/companyCards/amex.svg
@@ -1,40 +1 @@
-
-
-
+
\ No newline at end of file
diff --git a/assets/images/companyCards/card-amex.svg b/assets/images/companyCards/card-amex.svg
index 0e8b2d22e9b4..816b3ce3d9f3 100644
--- a/assets/images/companyCards/card-amex.svg
+++ b/assets/images/companyCards/card-amex.svg
@@ -1,32 +1 @@
-
-
-
+
\ No newline at end of file
diff --git a/assets/images/companyCards/card-bofa.svg b/assets/images/companyCards/card-bofa.svg
index 469142e4d6ff..3cc7cf1de2cc 100644
--- a/assets/images/companyCards/card-bofa.svg
+++ b/assets/images/companyCards/card-bofa.svg
@@ -1,32 +1 @@
-
-
-
+
\ No newline at end of file
diff --git a/assets/images/companyCards/card-brex.svg b/assets/images/companyCards/card-brex.svg
index dd19403d5837..d2511fb4bf31 100644
--- a/assets/images/companyCards/card-brex.svg
+++ b/assets/images/companyCards/card-brex.svg
@@ -1,27 +1 @@
-
-
-
+
\ No newline at end of file
diff --git a/assets/images/companyCards/card-capital_one.svg b/assets/images/companyCards/card-capital_one.svg
index 0a324710ae5d..64e79b8745db 100644
--- a/assets/images/companyCards/card-capital_one.svg
+++ b/assets/images/companyCards/card-capital_one.svg
@@ -1,42 +1 @@
-
-
\ No newline at end of file
+
\ No newline at end of file
diff --git a/assets/images/companyCards/card-capitalone.svg b/assets/images/companyCards/card-capitalone.svg
index 95948992383b..a7c54c7bf529 100644
--- a/assets/images/companyCards/card-capitalone.svg
+++ b/assets/images/companyCards/card-capitalone.svg
@@ -1,27 +1 @@
-
-
-
+
\ No newline at end of file
diff --git a/assets/images/companyCards/card-chase.svg b/assets/images/companyCards/card-chase.svg
index 7bea71bd66ec..e0f539eeb766 100644
--- a/assets/images/companyCards/card-chase.svg
+++ b/assets/images/companyCards/card-chase.svg
@@ -1,24 +1 @@
-
-
-
+
\ No newline at end of file
diff --git a/assets/images/companyCards/card-citi.svg b/assets/images/companyCards/card-citi.svg
index c8d71afd7798..9c35e1b1ea4f 100644
--- a/assets/images/companyCards/card-citi.svg
+++ b/assets/images/companyCards/card-citi.svg
@@ -1,32 +1 @@
-
-
-
+
\ No newline at end of file
diff --git a/assets/images/companyCards/card-expensify.svg b/assets/images/companyCards/card-expensify.svg
index 9fd29b511c7b..3763b50e4b8a 100644
--- a/assets/images/companyCards/card-expensify.svg
+++ b/assets/images/companyCards/card-expensify.svg
@@ -1,99 +1 @@
-
-
-
+
\ No newline at end of file
diff --git a/assets/images/companyCards/card-mastercard.svg b/assets/images/companyCards/card-mastercard.svg
index e8d3cf8f4096..d8f90ea1f186 100644
--- a/assets/images/companyCards/card-mastercard.svg
+++ b/assets/images/companyCards/card-mastercard.svg
@@ -1,27 +1 @@
-
-
-
+
\ No newline at end of file
diff --git a/assets/images/companyCards/card-stripe.svg b/assets/images/companyCards/card-stripe.svg
index 608f067a1854..a618dc96af78 100644
--- a/assets/images/companyCards/card-stripe.svg
+++ b/assets/images/companyCards/card-stripe.svg
@@ -1,39 +1 @@
-
-
-
+
\ No newline at end of file
diff --git a/assets/images/companyCards/card-visa.svg b/assets/images/companyCards/card-visa.svg
index 9e2eae97ba90..dd8ca795403d 100644
--- a/assets/images/companyCards/card-visa.svg
+++ b/assets/images/companyCards/card-visa.svg
@@ -1,73 +1 @@
-
-
-
+
\ No newline at end of file
diff --git a/assets/images/companyCards/card-wells_fargo.svg b/assets/images/companyCards/card-wells_fargo.svg
index 66402710de97..8bb8b54bbbd4 100644
--- a/assets/images/companyCards/card-wells_fargo.svg
+++ b/assets/images/companyCards/card-wells_fargo.svg
@@ -1,35 +1 @@
-
-
\ No newline at end of file
+
\ No newline at end of file
diff --git a/assets/images/companyCards/card-wellsfargo.svg b/assets/images/companyCards/card-wellsfargo.svg
index 086f66cc0423..bf9ea49ee2bd 100644
--- a/assets/images/companyCards/card-wellsfargo.svg
+++ b/assets/images/companyCards/card-wellsfargo.svg
@@ -1,57 +1 @@
-
-
-
+
\ No newline at end of file
diff --git a/assets/images/companyCards/card=-generic.svg b/assets/images/companyCards/card=-generic.svg
index 61e4296f7779..192c194da9e7 100644
--- a/assets/images/companyCards/card=-generic.svg
+++ b/assets/images/companyCards/card=-generic.svg
@@ -1,25 +1 @@
-
-
-
+
\ No newline at end of file
diff --git a/assets/images/companyCards/emptystate__card-pos.svg b/assets/images/companyCards/emptystate__card-pos.svg
index 6a6fbae74a04..e7f8429c254c 100644
--- a/assets/images/companyCards/emptystate__card-pos.svg
+++ b/assets/images/companyCards/emptystate__card-pos.svg
@@ -1,643 +1 @@
-
-
-
+
\ No newline at end of file
diff --git a/assets/images/companyCards/mastercard.svg b/assets/images/companyCards/mastercard.svg
index dcfac5eb33dd..24ff5d159c0b 100644
--- a/assets/images/companyCards/mastercard.svg
+++ b/assets/images/companyCards/mastercard.svg
@@ -1,40 +1 @@
-
-
-
+
\ No newline at end of file
diff --git a/assets/images/companyCards/pending-bank.svg b/assets/images/companyCards/pending-bank.svg
index dc265466d53f..58b7b96dab28 100644
--- a/assets/images/companyCards/pending-bank.svg
+++ b/assets/images/companyCards/pending-bank.svg
@@ -1,263 +1 @@
-
-
+
\ No newline at end of file
diff --git a/assets/images/companyCards/pendingstate_laptop-with-hourglass-and-cards.svg b/assets/images/companyCards/pendingstate_laptop-with-hourglass-and-cards.svg
index 0f40859c8839..258b0d0bb7b4 100644
--- a/assets/images/companyCards/pendingstate_laptop-with-hourglass-and-cards.svg
+++ b/assets/images/companyCards/pendingstate_laptop-with-hourglass-and-cards.svg
@@ -1,244 +1 @@
-
-
\ No newline at end of file
+
\ No newline at end of file
diff --git a/assets/images/companyCards/visa.svg b/assets/images/companyCards/visa.svg
index 4a7a73b66639..4195eb76442a 100644
--- a/assets/images/companyCards/visa.svg
+++ b/assets/images/companyCards/visa.svg
@@ -1,74 +1 @@
-
-
-
+
\ No newline at end of file
diff --git a/assets/images/expensify-card-icon.svg b/assets/images/expensify-card-icon.svg
index 8680b7a22878..ab78635a8d23 100644
--- a/assets/images/expensify-card-icon.svg
+++ b/assets/images/expensify-card-icon.svg
@@ -1,16 +1 @@
-
-
\ No newline at end of file
+
\ No newline at end of file
diff --git a/assets/images/expensify-card.svg b/assets/images/expensify-card.svg
index 2989f5025ae4..9614ef4955cc 100644
--- a/assets/images/expensify-card.svg
+++ b/assets/images/expensify-card.svg
@@ -1 +1 @@
-
+
\ No newline at end of file
diff --git a/assets/images/gallery-not-found.svg b/assets/images/gallery-not-found.svg
index 25da973ce9cb..87231be3741b 100644
--- a/assets/images/gallery-not-found.svg
+++ b/assets/images/gallery-not-found.svg
@@ -1,18 +1 @@
-
-
\ No newline at end of file
+
\ No newline at end of file
diff --git a/assets/images/laptop-with-second-screen-sync.svg b/assets/images/laptop-with-second-screen-sync.svg
index a74048795dbf..153825d36415 100644
--- a/assets/images/laptop-with-second-screen-sync.svg
+++ b/assets/images/laptop-with-second-screen-sync.svg
@@ -1,213 +1 @@
-
-
\ No newline at end of file
+
\ No newline at end of file
diff --git a/assets/images/laptop-with-second-screen-x.svg b/assets/images/laptop-with-second-screen-x.svg
index f4b6b77f70f1..8d051989bca4 100644
--- a/assets/images/laptop-with-second-screen-x.svg
+++ b/assets/images/laptop-with-second-screen-x.svg
@@ -1,150 +1 @@
-
+
\ No newline at end of file
diff --git a/assets/images/product-illustrations/broken-magnifying-glass.svg b/assets/images/product-illustrations/broken-magnifying-glass.svg
index 0b85744c1869..14de9eff24c1 100644
--- a/assets/images/product-illustrations/broken-magnifying-glass.svg
+++ b/assets/images/product-illustrations/broken-magnifying-glass.svg
@@ -1,28 +1 @@
-
+
\ No newline at end of file
diff --git a/assets/images/simple-illustrations/emptystate__puzzlepieces.svg b/assets/images/simple-illustrations/emptystate__puzzlepieces.svg
new file mode 100644
index 000000000000..d137ce5dcff2
--- /dev/null
+++ b/assets/images/simple-illustrations/emptystate__puzzlepieces.svg
@@ -0,0 +1,93 @@
+
+
+
diff --git a/assets/images/simple-illustrations/simple-illustration__commentbubbles_blue.svg b/assets/images/simple-illustrations/simple-illustration__commentbubbles_blue.svg
index 9c0711fcaedc..1a99094d07d9 100644
--- a/assets/images/simple-illustrations/simple-illustration__commentbubbles_blue.svg
+++ b/assets/images/simple-illustrations/simple-illustration__commentbubbles_blue.svg
@@ -1,22 +1 @@
-
-
-
+
\ No newline at end of file
diff --git a/assets/images/simple-illustrations/simple-illustration__envelopereceipt.svg b/assets/images/simple-illustrations/simple-illustration__envelopereceipt.svg
index eb2bad31620d..496255692f8c 100644
--- a/assets/images/simple-illustrations/simple-illustration__envelopereceipt.svg
+++ b/assets/images/simple-illustrations/simple-illustration__envelopereceipt.svg
@@ -1,49 +1 @@
-
-
\ No newline at end of file
+
\ No newline at end of file
diff --git a/assets/images/simple-illustrations/simple-illustration__magnifyingglass-money.svg b/assets/images/simple-illustrations/simple-illustration__magnifyingglass-money.svg
index e7f64f69305a..3bb3514f1ebc 100644
--- a/assets/images/simple-illustrations/simple-illustration__magnifyingglass-money.svg
+++ b/assets/images/simple-illustrations/simple-illustration__magnifyingglass-money.svg
@@ -1,49 +1 @@
-
-
-
+
\ No newline at end of file
diff --git a/assets/images/simple-illustrations/simple-illustration__rules.svg b/assets/images/simple-illustrations/simple-illustration__rules.svg
index 6432f26d9ac6..5646cc0f5c2a 100644
--- a/assets/images/simple-illustrations/simple-illustration__rules.svg
+++ b/assets/images/simple-illustrations/simple-illustration__rules.svg
@@ -1,10 +1 @@
-
+
\ No newline at end of file
diff --git a/assets/images/spreadsheet-computer.svg b/assets/images/spreadsheet-computer.svg
index 74cac455537a..1a42220c8d86 100644
--- a/assets/images/spreadsheet-computer.svg
+++ b/assets/images/spreadsheet-computer.svg
@@ -1,186 +1 @@
-
+
\ No newline at end of file
diff --git a/assets/images/table.svg b/assets/images/table.svg
index dea1e990b97d..8a77919aa5a5 100644
--- a/assets/images/table.svg
+++ b/assets/images/table.svg
@@ -1,3 +1 @@
-
+
\ No newline at end of file
diff --git a/assets/images/turtle-in-shell.svg b/assets/images/turtle-in-shell.svg
index 6c5a8e74bb31..631aeb6b0940 100644
--- a/assets/images/turtle-in-shell.svg
+++ b/assets/images/turtle-in-shell.svg
@@ -1,87 +1 @@
-
-
\ No newline at end of file
+
\ No newline at end of file
diff --git a/assets/images/user-eye.svg b/assets/images/user-eye.svg
index 2265b4892ded..7aa640b180d1 100644
--- a/assets/images/user-eye.svg
+++ b/assets/images/user-eye.svg
@@ -1,12 +1 @@
-
-
\ No newline at end of file
+
\ No newline at end of file
diff --git a/assets/images/user-plus.svg b/assets/images/user-plus.svg
index bd49633bf738..84af850da735 100644
--- a/assets/images/user-plus.svg
+++ b/assets/images/user-plus.svg
@@ -1,11 +1 @@
-
-
\ No newline at end of file
+
\ No newline at end of file
diff --git a/config/webpack/CustomVersionFilePlugin.ts b/config/webpack/CustomVersionFilePlugin.ts
index 96ab8e61e480..1e442d55325e 100644
--- a/config/webpack/CustomVersionFilePlugin.ts
+++ b/config/webpack/CustomVersionFilePlugin.ts
@@ -4,23 +4,31 @@ import type {Compiler} from 'webpack';
import {version as APP_VERSION} from '../../package.json';
/**
- * Simple webpack plugin that writes the app version (from package.json) and the webpack hash to './version.json'
+ * Custom webpack plugin that writes the app version (from package.json) and the webpack hash to './version.json'
*/
class CustomVersionFilePlugin {
apply(compiler: Compiler) {
compiler.hooks.done.tap(this.constructor.name, () => {
const versionPath = path.join(__dirname, '/../../dist/version.json');
- fs.mkdir(path.dirname(versionPath), {recursive: true}, (directoryError) => {
- if (directoryError) {
- throw directoryError;
- }
- fs.writeFile(versionPath, JSON.stringify({version: APP_VERSION}), {encoding: 'utf8'}, (error) => {
- if (!error) {
- return;
+
+ fs.promises
+ .mkdir(path.dirname(versionPath), {recursive: true})
+ .then(() => fs.promises.readFile(versionPath, 'utf8'))
+ .then((existingVersion) => {
+ const {version} = JSON.parse(existingVersion) as {version: string};
+
+ if (version !== APP_VERSION) {
+ fs.promises.writeFile(versionPath, JSON.stringify({version: APP_VERSION}), 'utf8');
+ }
+ })
+ .catch((err: NodeJS.ErrnoException) => {
+ if (err.code === 'ENOENT') {
+ // if file doesn't exist
+ fs.promises.writeFile(versionPath, JSON.stringify({version: APP_VERSION}), 'utf8');
+ } else {
+ throw err;
}
- throw error;
});
- });
});
}
}
diff --git a/config/webpack/webpack.dev.ts b/config/webpack/webpack.dev.ts
index 80813adc1e3a..2279082024d1 100644
--- a/config/webpack/webpack.dev.ts
+++ b/config/webpack/webpack.dev.ts
@@ -1,3 +1,5 @@
+/* eslint-disable @typescript-eslint/naming-convention */
+import ReactRefreshWebpackPlugin from '@pmmmwh/react-refresh-webpack-plugin';
import path from 'path';
import portfinder from 'portfinder';
import {TimeAnalyticsPlugin} from 'time-analytics-webpack-plugin';
@@ -54,15 +56,15 @@ const getConfiguration = (environment: Environment): Promise =>
},
},
headers: {
- // eslint-disable-next-line @typescript-eslint/naming-convention
'Document-Policy': 'js-profiling',
},
},
plugins: [
new DefinePlugin({
- // eslint-disable-next-line @typescript-eslint/naming-convention
'process.env.PORT': port,
+ 'process.env.NODE_ENV': JSON.stringify('development'),
}),
+ new ReactRefreshWebpackPlugin({overlay: {sockProtocol: 'wss'}}),
],
cache: {
type: 'filesystem',
@@ -82,7 +84,7 @@ const getConfiguration = (environment: Environment): Promise =>
},
});
- return TimeAnalyticsPlugin.wrap(config);
+ return TimeAnalyticsPlugin.wrap(config, {plugin: {exclude: ['ReactRefreshPlugin']}});
});
export default getConfiguration;
diff --git a/tests/perf-test/README.md b/contributingGuides/REASSURE_PERFORMANCE_TEST.md
similarity index 90%
rename from tests/perf-test/README.md
rename to contributingGuides/REASSURE_PERFORMANCE_TEST.md
index 2b66f7c147f3..0de450b78875 100644
--- a/tests/perf-test/README.md
+++ b/contributingGuides/REASSURE_PERFORMANCE_TEST.md
@@ -7,8 +7,11 @@ We use Reassure for monitoring performance regression. It helps us check if our
- Reassure builds on the existing React Testing Library setup and adds a performance measurement functionality. It's intended to be used on local machine and on a remote server as part of your continuous integration setup.
- To make sure the results are reliable and consistent, Reassure runs tests twice – once for the current branch and once for the base branch.
-## Performance Testing Strategy (`measurePerformance`)
+## Performance Testing Strategy (`measureRenders`)
+- Before adding new tests, check if the proposed scenario or component is already covered in existing tests. Duplicate tests can slow down the CI suite, making it harder to spot meaningful regressions.
+- Test only scenarios that cover new or unique interactions. Avoid testing repetitive user actions that could be captured within a single, comprehensive scenario.
+- Where applicable, use utility functions and helper methods to consolidate common actions (e.g., data mocking, scenario setup) across tests. This reduces redundancy and allows tests to be more focused and reusable.
- The primary focus is on testing business cases rather than small, reusable parts that typically don't introduce regressions, although some tests in that area are still necessary.
- To achieve this goal, it's recommended to stay relatively high up in the React tree, targeting whole screens to recreate real-life scenarios that users may encounter.
- For example, consider scenarios where an additional `useMemo` call could impact performance negatively.
@@ -84,7 +87,7 @@ test('Count increments on press', async () => {
await screen.findByText('Count: 2');
};
- await measurePerformance(
+ await measureRenders(
,
{ scenario, runs: 20 }
);
diff --git a/contributingGuides/REVIEWER_CHECKLIST.md b/contributingGuides/REVIEWER_CHECKLIST.md
index 4ff1f01b1475..5fc14328f3b4 100644
--- a/contributingGuides/REVIEWER_CHECKLIST.md
+++ b/contributingGuides/REVIEWER_CHECKLIST.md
@@ -19,7 +19,6 @@
- [ ] If there are any errors in the console that are unrelated to this PR, I either fixed them (preferred) or linked to where I reported them in Slack
- [ ] I verified proper code patterns were followed (see [Reviewing the code](https://github.com/Expensify/App/blob/main/contributingGuides/PR_REVIEW_GUIDELINES.md#reviewing-the-code))
- [ ] I verified that any callback methods that were added or modified are named for what the method does and never what callback they handle (i.e. `toggleReport` and not `onIconClick`).
- - [ ] I verified that the left part of a conditional rendering a React component is a boolean and NOT a string, e.g. `myBool && `.
- [ ] I verified that comments were added to code that is not self explanatory
- [ ] I verified that any new or modified comments were clear, correct English, and explained "why" the code was doing something instead of only explaining "what" the code was doing.
- [ ] I verified any copy / text shown in the product is localized by adding it to `src/languages/*` files and using the [translation method](https://github.com/Expensify/App/blob/4bd99402cebdf4d7394e0d1f260879ea238197eb/src/components/withLocalize.js#L60)
diff --git a/docs/articles/expensify-classic/bank-accounts-and-payments/payments/Pay-an-Invoice.md b/docs/articles/expensify-classic/bank-accounts-and-payments/payments/Pay-an-Invoice.md
index 84fafc949527..18020402f7de 100644
--- a/docs/articles/expensify-classic/bank-accounts-and-payments/payments/Pay-an-Invoice.md
+++ b/docs/articles/expensify-classic/bank-accounts-and-payments/payments/Pay-an-Invoice.md
@@ -10,9 +10,15 @@ There are multiple ways to pay Invoices in Expensify. Let’s go over each metho
# How to Pay Invoices
1. Sign in to your [Expensify web account](www.expensify.com).
-2. Click on the Invoice you’d like to pay to see the details.
-3. Click on the **Pay** button.
-4. Follow the prompts to pay through one of the following methods.
+2. Click on **Home** and find the pending Invoice payment
+3. Click **Pay** to be redirected to the Invoice
+4. Review the Invoice
+5. When you are ready to pay, click the **Pay** button at the top of the Invoice
+6. Follow the prompts to pay through one of the following methods.
+
+![Click Home and Pay on the invoice](https://help.expensify.com/assets/images/ExpensifyHelp_OldDot_PayInvoice_1.png){:width="100%"}
+
+![Click Pay on Invoice and choose a method of payment](https://help.expensify.com/assets/images/ExpensifyHelp_OldDot_PayInvoice_2.png){:width="100%"}
### ACH bank-to-bank transfer
diff --git a/docs/articles/expensify-classic/expenses/Add-Invoices-in-Bulk.md b/docs/articles/expensify-classic/expenses/Add-Invoices-in-Bulk.md
index 6257c1e6d84d..3e9b6c0397db 100644
--- a/docs/articles/expensify-classic/expenses/Add-Invoices-in-Bulk.md
+++ b/docs/articles/expensify-classic/expenses/Add-Invoices-in-Bulk.md
@@ -14,6 +14,13 @@ Expensify offers importing multiple invoices (bulk import) via CSV to save you f
5. Add the invoice details following the formatting rules (see below **CSV formatting guide** section)
6. Click **Upload CSV**
+![Click Reports, New Reports, choose Bulk Import Invoices](https://help.expensify.com/assets/images/invoice-bulk-01.png){:width="100%"}
+
+![Download Sample CSV](https://help.expensify.com/assets/images/invoice-bulk-02.png){:width="100%"}
+
+![Format CSV following our guidelines](https://help.expensify.com/assets/images/invoice-bulk-03.png){:width="100%"}
+
+
## CSV formatting guide
- Send to: recipient's email address (ex: john.smith@companydomain.com)
@@ -27,10 +34,15 @@ Expensify offers importing multiple invoices (bulk import) via CSV to save you f
## After the Invoices are uploaded
- After you click **Upload**, the invoices will automatically be created and viewable on the **Reports** page.
+- Set the **Reports page** filter to Invoices to narrow down your search.
- The **Send To** contact will get an email notifying them of the invoice you sent.
- You can manually edit the invoice details.
- You can manually upload a PDF of the invoice to the report.
+![Search for Invoices on Reports page](https://help.expensify.com/assets/images/invoice-bulk-04.png){:width="100%"}
+
+![Invoices will indicate next steps at the top of each report](https://help.expensify.com/assets/images/invoice-bulk-05.png){:width="100%"}
+
{% include faq-begin.md %}
## Are there any fees associated with Invoices in Expensify?
diff --git a/docs/articles/expensify-classic/expenses/Add-an-expense.md b/docs/articles/expensify-classic/expenses/Add-an-expense.md
index 461748c6af9e..92a96e989013 100644
--- a/docs/articles/expensify-classic/expenses/Add-an-expense.md
+++ b/docs/articles/expensify-classic/expenses/Add-an-expense.md
@@ -88,7 +88,7 @@ You can also email receipts to SmartScan by sending them to receipts@expensify.c
If you are an employee under a company workspace, you may not see all of the different expense type options depending on your company’s workspace settings.
{% include end-info.html %}
-# FAQs
+{% include faq-begin.md %}
**What’s the difference between a reimbursable and non-reimbursable expense?**
@@ -99,4 +99,5 @@ If you are an employee under a company workspace, you may not see all of the dif
If you are an employee under a company workspace, your expenses may automatically be configured as reimbursable or non-reimbursable depending on the details that are entered. If an expense is incorrectly labeled, you must reach out to an admin to have it corrected.
{% include end-info.html %}
+{% include faq-end.md %}
diff --git a/docs/articles/expensify-classic/expenses/Send-and-Receive-Payment-for-Invoices.md b/docs/articles/expensify-classic/expenses/Send-and-Receive-Payment-for-Invoices.md
index c2ebb64b0af6..833fbdc4b200 100644
--- a/docs/articles/expensify-classic/expenses/Send-and-Receive-Payment-for-Invoices.md
+++ b/docs/articles/expensify-classic/expenses/Send-and-Receive-Payment-for-Invoices.md
@@ -11,9 +11,18 @@ Invoices can be sent to anyone with or without an Expensify account and paid dir
1. Sign in to your [Expensify web account](www.expensify.com)
2. Customize your company invoices following the steps in this [help article](https://help.expensify.com/articles/expensify-classic/workspaces/Set-Up-Invoicing). (Optional)
3. From the **Reports** page, click the drop-down and select **Invoice**.
-4. Upload a PDF/image of the invoice.
-5. Add applicable tags and categories based on your workspace settings.
-6. Click **Send**.
+4. Click **Add Expense** to upload an invoice or drag and drop the invoice as a pdf into the report to start the SmartScan process.
+5. Once the SmartScan process is complete, the invoice PDF will be added as a receipt to the expense
+6. Add applicable tags and categories based on your workspace settings.
+7. Click **Send**
+8. Enter the recipient's email address
+9. Add a memo, due date, attach a PDF of the invoice (Optional)
+10. Click **Send**
+11. The recipient will receive an email about the invoice and can pay through Expensify following these [steps](https://help.expensify.com/articles/expensify-classic/bank-accounts-and-payments/payments/Pay-an-Invoice).
+
+![From the Reports page, click New Report and select Invoice](https://help.expensify.com/assets/images/ExpensifyHelp_OldDot_SendInvoice.png){:width="100%"}
+
+![Click Send and enter the recipients email address](https://help.expensify.com/assets/images/ExpensifyHelp_OldDot_SendInvoice.png){:width="100%"}
## How to Receive an Invoice Payment in Expensify
diff --git a/docs/articles/new-expensify/billing-and-subscriptions/Billing-page-coming-soon.md b/docs/articles/new-expensify/billing-and-subscriptions/Billing-page-coming-soon.md
deleted file mode 100644
index 2ae2fcd2426d..000000000000
--- a/docs/articles/new-expensify/billing-and-subscriptions/Billing-page-coming-soon.md
+++ /dev/null
@@ -1,6 +0,0 @@
----
-title: Billing and Subscriptions
-description: Coming soon
----
-
-# Coming Soon
diff --git a/docs/articles/new-expensify/billing-and-subscriptions/Billing-page.md b/docs/articles/new-expensify/billing-and-subscriptions/Billing-page.md
new file mode 100644
index 000000000000..f945840d65da
--- /dev/null
+++ b/docs/articles/new-expensify/billing-and-subscriptions/Billing-page.md
@@ -0,0 +1,6 @@
+---
+title: Billing and Subscriptions
+description: An overview of how billing works in Expensify.
+---
+
+# Coming Soon
diff --git a/docs/articles/new-expensify/connections/quickbooks-online/Connect-to-QuickBooks-Online.md b/docs/articles/new-expensify/connections/quickbooks-online/Connect-to-QuickBooks-Online.md
index 60fdbe94b33b..192f7bf172b6 100644
--- a/docs/articles/new-expensify/connections/quickbooks-online/Connect-to-QuickBooks-Online.md
+++ b/docs/articles/new-expensify/connections/quickbooks-online/Connect-to-QuickBooks-Online.md
@@ -56,73 +56,6 @@ Log in to QuickBooks Online and ensure all of your employees are setup as either
![The QuickBooks Online Connect Connect button]({{site.url}}/assets/images/ExpensifyHelp-QBO-5.png){:width="100%"}
-
-
-# Step 3: Configure import settings
-
-The following steps help you determine how data will be imported from QuickBooks Online to Expensify.
-
-
-
Under the Accounting settings for your workspace, click Import under the QuickBooks Online connection.
-
Review each of the following import settings:
-
-
Chart of accounts: The chart of accounts are automatically imported from QuickBooks Online as categories. This cannot be amended.
-
Classes: Choose whether to import classes, which will be shown in Expensify as tags for expense-level coding.
-
Customers/projects: Choose whether to import customers/projects, which will be shown in Expensify as tags for expense-level coding.
-
Locations: Choose whether to import locations, which will be shown in Expensify as tags for expense-level coding.
-{% include info.html %}
-As Locations are only configurable as tags, you cannot export expense reports as vendor bills or checks to QuickBooks Online. To unlock these export options, either disable locations import or upgrade to the Control Plan to export locations encoded as a report field.
-{% include end-info.html %}
-
Taxes: Choose whether to import tax rates and defaults.
-
-
-
-# Step 4: Configure export settings
-
-The following steps help you determine how data will be exported from Expensify to QuickBooks Online.
-
-
-
Under the Accounting settings for your workspace, click Export under the QuickBooks Online connection.
-
Review each of the following export settings:
-
-
Preferred Exporter: Choose whether to assign a Workspace Admin as the Preferred Exporter. Once selected, the Preferred Exporter automatically receives reports for export in their account to help automate the exporting process.
-
-{% include info.html %}
-* Other Workspace Admins will still be able to export to QuickBooks Online.
-* If you set different export accounts for individual company cards under your domain settings, then your Preferred Exporter must be a Domain Admin.
-{% include end-info.html %}
-
-
Date: Choose whether to use the date of last expense, export date, or submitted date.
-
Export Out-of-Pocket Expenses as: Select whether out-of-pocket expenses will be exported as a check, journal entry, or vendor bill.
-
-{% include info.html %}
-These settings may vary based on whether tax is enabled for your workspace.
-* If tax is not enabled on the workspace, you’ll also select the Accounts Payable/AP.
-* If tax is enabled on the workspace, journal entry will not be available as an option. If you select the journal entries option first and later enable tax on the workspace, you will see a red dot and an error message under the “Export Out-of-Pocket Expenses as” options. To resolve this error, you must change your export option to vendor bill or check to successfully code and export expense reports.
-{% include end-info.html %}
-
-
Invoices: Select the QuickBooks Online invoice account that invoices will be exported to.
-
Export as: Select whether company cards export to QuickBooks Online as a credit card (the default), debit card, or vendor bill. Then select the account they will export to.
-
If you select vendor bill, you’ll also select the accounts payable account that vendor bills will be created from, as well as whether to set a default vendor for credit card transactions upon export. If this option is enabled, you will select the vendor that all credit card transactions will be applied to.
-
-
-
-# Step 5: Configure advanced settings
-
-The following steps help you determine the advanced settings for your connection, like auto-sync and employee invitation settings.
-
-
-
Under the Accounting settings for your workspace, click Advanced under the QuickBooks Online connection.
-
Select an option for each of the following settings:
-
-
Auto-sync: Choose whether to enable QuickBooks Online to automatically communicate changes with Expensify to ensure that the data shared between the two systems is up-to-date. New report approvals/reimbursements will be synced during the next auto-sync period.
-
Invite Employees: Choose whether to enable Expensify to import employee records from QuickBooks Online and invite them to this workspace.
-
Automatically Create Entities: Choose whether to enable Expensify to automatically create vendors and customers in QuickBooks Online if a matching vendor or customer does not exist.
-
Sync Reimbursed Reports: Choose whether to enable report syncing for reimbursed expenses. If enabled, all reports that are marked as Paid in QuickBooks Online will also show in Expensify as Paid. If enabled, you must also select the QuickBooks Online account that reimbursements are coming out of, and Expensify will automatically create the payment in QuickBooks Online.
-
Invoice Collection Account: Select the invoice collection account that you want invoices to appear under once the invoice is marked as paid.
-
-
-
{% include faq-begin.md %}
**Why do I see a red dot next to my connection?**
diff --git a/docs/articles/new-expensify/connections/xero/Configure-Xero.md b/docs/articles/new-expensify/connections/xero/Configure-Xero.md
index 218e81c98707..b417d6169a1e 100644
--- a/docs/articles/new-expensify/connections/xero/Configure-Xero.md
+++ b/docs/articles/new-expensify/connections/xero/Configure-Xero.md
@@ -1,11 +1,75 @@
---
title: Configure Xero
-description: Coming soon
+description: How to configure your settings for Xero
---
+
+To configure your Xero settings, complete the steps below.
-# FAQ
+# Step 1: Configure import settings
-## How do I know if a report successfully exported to Xero?
+The following steps help you determine how data will be imported from Xero to Expensify.
+
+
+
Under the Accounting settings for your workspace, click Import under the Xero connection.
+
Select an option for each of the following settings to determine what information will be imported from Xero into Expensify:
+
+
Xero organization: Select which Xero organization your Expensify workspace is connected to. Each organization can only be connected to one workspace at a time.
+
Chart of Accounts: Your Xero chart of accounts and any accounts marked as “Show In Expense Claims” will be automatically imported into Expensify as Categories. This cannot be amended.
+
Tracking Categories: Choose whether to import your Xero categories for cost centers and regions as tags in Expensify.
+
Re-bill Customers: When enabled, Xero customer contacts are imported into Expensify as tags for expense tracking. After exporting to Xero, tagged billable expenses can be included on a sales invoice to your customer.
+
Taxes: Choose whether to import tax rates and tax defaults from Xero.
+
+
+
+# Step 2: Configure export settings
+The following steps help you determine how data will be exported from Expensify to Xero.
+
+
+
Under the Accounting settings for your workspace, click Export under the Xero connection.
+
Review each of the following export settings:
+
+
Preferred Exporter: Choose whether to assign a Workspace Admin as the Preferred Exporter. Once selected, the Preferred Exporter automatically receives reports for export in their account to help automate the exporting process.
+
+
+{% include info.html %}
+- Other Workspace Admins will still be able to export to Xero.
+- If you set different export accounts for individual company cards under your domain settings, then your Preferred Exporter must be a Domain Admin.
+{% include end-info.html %}
+
+
+
+
Export Out-of-Pocket Expenses as: All out-of-pocket expenses will be exported as purchase bills. This cannot be amended.
+
Purchase Bill Date: Choose whether to use the date of the last expense, export date, or submitted date.
+
Export invoices as: All invoices exported to Xero will be as sales invoices. This cannot be amended.
+
Export company card expenses as: All company card expenses are exported to Xero as bank transactions. This cannot be amended.
+
Xero Bank Account: Select which bank account will be used to post bank transactions when non-reimbursable expenses are exported.
+
+
+
+# Step 3: Configure advanced settings
+
+The following steps help you determine the advanced settings for your connection, like auto-sync.
+
+
+
Under the Accounting settings for your workspace, click Advanced under the Xero connection.
+
Select an option for each of the following settings:
+
+
Auto-sync: Choose whether to enable Xero to automatically communicate changes with Expensify to ensure that the data shared between the two systems is up-to-date. New report approvals/reimbursements will be synced during the next auto-sync period. Once you’ve added a business bank account for ACH reimbursement, any reimbursable expenses will be sent to Xero automatically when the report is reimbursed. For non-reimbursable reports, Expensify automatically queues the report to export to Xero after it has completed the approval workflow in Expensify.
+
Set Purchase Bill Status: Choose the status of your purchase bills:
+
+
Draft
+
Awaiting Approval
+
Awaiting Payment
+
+
Sync Reimbursed Reports: Choose whether to enable report syncing for reimbursed expenses. If enabled, all reports that are marked as Paid in Xero will also show in Expensify as Paid. If enabled, you must also select the Xero account that reimbursements are coming out of, and Expensify will automatically create the payment in Xero.
+
Xero Bill Payment Account: If you enable Sync Reimbursed Reports, you must select the Xero Bill Payment account your reimbursements will come from.
+
Xero Invoice Collections Account: If you are exporting invoices from Expensify, select the invoice collection account that you want invoices to appear under once they are marked as paid.
+
+
+
+{% include faq-begin.md %}
+
+## How do I know if a report is successfully exported to Xero?
When a report exports successfully, a message is posted in the related Expensify Chat room.
@@ -23,3 +87,5 @@ When an admin manually exports a report, Expensify will warn them if the report
- If a report has been exported and reimbursed via ACH, it will be automatically marked as paid in Xero during the next sync.
- If a report has been exported and marked as paid in Xero, it will be automatically marked as reimbursed in Expensify during the next sync.
- If a report has not yet been exported to Xero, it won’t be automatically exported.
+
+{% include faq-end.md %}
diff --git a/docs/articles/new-expensify/workspaces/Require-tags-and-categories-for-expenses.md b/docs/articles/new-expensify/workspaces/Require-tags-and-categories-for-expenses.md
index df77ed3b5b01..f2fd6970f5af 100644
--- a/docs/articles/new-expensify/workspaces/Require-tags-and-categories-for-expenses.md
+++ b/docs/articles/new-expensify/workspaces/Require-tags-and-categories-for-expenses.md
@@ -30,7 +30,7 @@ To require workspace members to add tags and/or categories to their expenses,
{% include end-selector.html %}
-![In the Workspace > Categories setting, the right-hand panel is open and the toggle to require categories on expenses is highlighted.]({{site.url}}/assets/images/workspace_category_toggle.png){:width="100%"}
+![In the Workspace, Categories setting, the right-hand panel is open and the toggle to require categories on expenses is highlighted.]({{site.url}}/assets/images/Workspace_category_toggle.png){:width="100%"}
This will highlight the tag and/or category field as required on all expenses.
diff --git a/docs/redirects.csv b/docs/redirects.csv
index d3672618cfad..06fd7c1ef502 100644
--- a/docs/redirects.csv
+++ b/docs/redirects.csv
@@ -590,3 +590,4 @@ https://help.expensify.com/articles/expensify-classic/articles/expensify-classic
https://help.expensify.com/articles/expensify-classic/articles/expensify-classic/expenses/Bulk-Upload-Multiple-Invoices,https://help.expensify.com/articles/expensify-classic/articles/expensify-classic/expenses/Add-Invoices-in-Bulk
https://help.expensify.com/articles/expensify-classic/bank-accounts-and-payments/payments/Pay-Bills,https://help.expensify.com/articles/expensify-classic/bank-accounts-and-payments/payments/Create-and-Pay-Bills
https://help.expensify.com/articles/new-expensify/billing-and-subscriptions/add-a-payment-card-and-view-your-subscription,https://help.expensify.com/articles/new-expensify/billing-and-subscriptions/Add-a-payment-card-and-view-your-subscription
+https://help.expensify.com/articles/new-expensify/billing-and-subscriptions/Billing-page-coming-soon,https://help.expensify.com/articles/new-expensify/billing-and-subscriptions/Billing-page
diff --git a/fastlane/Fastfile b/fastlane/Fastfile
index 3c85aa5ff3d1..beb0afd2c7c8 100644
--- a/fastlane/Fastfile
+++ b/fastlane/Fastfile
@@ -406,7 +406,7 @@ platform :ios do
distribute_external: true,
notify_external_testers: true,
changelog: "Thank you for beta testing New Expensify, this version includes bug fixes and improvements.",
- groups: ["Beta"],
+ groups: ["Applause", "Beta Testers", "Expensify Employees"],
demo_account_required: true,
beta_app_review_info: {
contact_email: ENV["APPLE_CONTACT_EMAIL"],
diff --git a/ios/NewExpensify/Info.plist b/ios/NewExpensify/Info.plist
index 38afb6234c87..060d01b44a94 100644
--- a/ios/NewExpensify/Info.plist
+++ b/ios/NewExpensify/Info.plist
@@ -19,7 +19,7 @@
CFBundlePackageTypeAPPLCFBundleShortVersionString
- 9.0.54
+ 9.0.55CFBundleSignature????CFBundleURLTypes
@@ -40,7 +40,7 @@
CFBundleVersion
- 9.0.54.11
+ 9.0.55.9FullStoryOrgId
diff --git a/ios/NewExpensifyTests/Info.plist b/ios/NewExpensifyTests/Info.plist
index 334b2c094b92..c953a0d4de59 100644
--- a/ios/NewExpensifyTests/Info.plist
+++ b/ios/NewExpensifyTests/Info.plist
@@ -15,10 +15,10 @@
CFBundlePackageTypeBNDLCFBundleShortVersionString
- 9.0.54
+ 9.0.55CFBundleSignature????CFBundleVersion
- 9.0.54.11
+ 9.0.55.9
diff --git a/ios/NotificationServiceExtension/Info.plist b/ios/NotificationServiceExtension/Info.plist
index dce7c37b72de..f8989c8ee055 100644
--- a/ios/NotificationServiceExtension/Info.plist
+++ b/ios/NotificationServiceExtension/Info.plist
@@ -11,9 +11,9 @@
CFBundleName$(PRODUCT_NAME)CFBundleShortVersionString
- 9.0.54
+ 9.0.55CFBundleVersion
- 9.0.54.11
+ 9.0.55.9NSExtensionNSExtensionPointIdentifier
diff --git a/patches/react-native+0.75.2+018+Add-regex-to-TextInput.patch b/patches/react-native+0.75.2+018+Add-regex-to-TextInput.patch
new file mode 100644
index 000000000000..6c511d8cbec1
--- /dev/null
+++ b/patches/react-native+0.75.2+018+Add-regex-to-TextInput.patch
@@ -0,0 +1,299 @@
+diff --git a/node_modules/react-native/Libraries/Components/TextInput/AndroidTextInputNativeComponent.js b/node_modules/react-native/Libraries/Components/TextInput/AndroidTextInputNativeComponent.js
+index 770dfee..73e439b 100644
+--- a/node_modules/react-native/Libraries/Components/TextInput/AndroidTextInputNativeComponent.js
++++ b/node_modules/react-native/Libraries/Components/TextInput/AndroidTextInputNativeComponent.js
+@@ -329,6 +329,12 @@ export type NativeProps = $ReadOnly<{|
+ */
+ returnKeyType?: WithDefault,
+
++ /**
++ * Restricts the text value to match the specified regular expression. Use this
++ * instead of implementing the logic in JS to avoid flicker.
++ */
++ regex?: ?string,
++
+ /**
+ * Limits the maximum number of characters that can be entered. Use this
+ * instead of implementing the logic in JS to avoid flicker.
+@@ -699,6 +705,7 @@ export const __INTERNAL_VIEW_CONFIG: PartialViewConfig = {
+ process: require('../../StyleSheet/processColor').default,
+ },
+ maxLength: true,
++ regex: true,
+ selectTextOnFocus: true,
+ textShadowRadius: true,
+ underlineColorAndroid: {
+diff --git a/node_modules/react-native/Libraries/Components/TextInput/RCTTextInputViewConfig.js b/node_modules/react-native/Libraries/Components/TextInput/RCTTextInputViewConfig.js
+index dbfe5d5..1f359ba 100644
+--- a/node_modules/react-native/Libraries/Components/TextInput/RCTTextInputViewConfig.js
++++ b/node_modules/react-native/Libraries/Components/TextInput/RCTTextInputViewConfig.js
+@@ -151,6 +151,7 @@ const RCTTextInputViewConfig = {
+ autoFocus: true,
+ lineBreakStrategyIOS: true,
+ smartInsertDelete: true,
++ regex: true,
+ ...ConditionallyIgnoredEventHandlers({
+ onClear: true,
+ onChange: true,
+diff --git a/node_modules/react-native/Libraries/Components/TextInput/TextInput.d.ts b/node_modules/react-native/Libraries/Components/TextInput/TextInput.d.ts
+index 20501f7..76f30b9 100644
+--- a/node_modules/react-native/Libraries/Components/TextInput/TextInput.d.ts
++++ b/node_modules/react-native/Libraries/Components/TextInput/TextInput.d.ts
+@@ -701,6 +701,12 @@ export interface TextInputProps
+ */
+ inputMode?: InputModeOptions | undefined;
+
++ /**
++ * Restricts the text value to match the specified regular expression. Use this
++ * instead of implementing the logic in JS to avoid flicker.
++ */
++ regex?: string | undefined;
++
+ /**
+ * Limits the maximum number of characters that can be entered.
+ * Use this instead of implementing the logic in JS to avoid flicker.
+diff --git a/node_modules/react-native/Libraries/Components/TextInput/TextInput.flow.js b/node_modules/react-native/Libraries/Components/TextInput/TextInput.flow.js
+index 2f35731..5bb94bc 100644
+--- a/node_modules/react-native/Libraries/Components/TextInput/TextInput.flow.js
++++ b/node_modules/react-native/Libraries/Components/TextInput/TextInput.flow.js
+@@ -697,6 +697,12 @@ export type Props = $ReadOnly<{|
+ */
+ maxFontSizeMultiplier?: ?number,
+
++ /**
++ * Restricts the text value to match the specified regular expression. Use this
++ * instead of implementing the logic in JS to avoid flicker.
++ */
++ regex?: ?string,
++
+ /**
+ * Limits the maximum number of characters that can be entered. Use this
+ * instead of implementing the logic in JS to avoid flicker.
+diff --git a/node_modules/react-native/Libraries/Components/TextInput/TextInput.js b/node_modules/react-native/Libraries/Components/TextInput/TextInput.js
+index 8cfde15..4f3345c 100644
+--- a/node_modules/react-native/Libraries/Components/TextInput/TextInput.js
++++ b/node_modules/react-native/Libraries/Components/TextInput/TextInput.js
+@@ -731,6 +731,12 @@ export type Props = $ReadOnly<{|
+ */
+ maxFontSizeMultiplier?: ?number,
+
++ /**
++ * Restricts the text value to match the specified regular expression. Use this
++ * instead of implementing the logic in JS to avoid flicker.
++ */
++ regex?: ?string,
++
+ /**
+ * Limits the maximum number of characters that can be entered. Use this
+ * instead of implementing the logic in JS to avoid flicker.
+diff --git a/node_modules/react-native/Libraries/Text/TextInput/RCTBaseTextInputViewManager.mm b/node_modules/react-native/Libraries/Text/TextInput/RCTBaseTextInputViewManager.mm
+index e367394..95f21f2 100644
+--- a/node_modules/react-native/Libraries/Text/TextInput/RCTBaseTextInputViewManager.mm
++++ b/node_modules/react-native/Libraries/Text/TextInput/RCTBaseTextInputViewManager.mm
+@@ -59,6 +59,7 @@ @implementation RCTBaseTextInputViewManager {
+ RCT_EXPORT_VIEW_PROPERTY(inputAccessoryViewID, NSString)
+ RCT_EXPORT_VIEW_PROPERTY(textContentType, NSString)
+ RCT_EXPORT_VIEW_PROPERTY(passwordRules, NSString)
++RCT_EXPORT_VIEW_PROPERTY(regex, NSString)
+
+ RCT_EXPORT_VIEW_PROPERTY(onChange, RCTBubblingEventBlock)
+ RCT_EXPORT_VIEW_PROPERTY(onKeyPressSync, RCTDirectEventBlock)
+diff --git a/node_modules/react-native/React/Fabric/Mounting/ComponentViews/TextInput/RCTTextInputComponentView.mm b/node_modules/react-native/React/Fabric/Mounting/ComponentViews/TextInput/RCTTextInputComponentView.mm
+index db7cba4..f85f95a 100644
+--- a/node_modules/react-native/React/Fabric/Mounting/ComponentViews/TextInput/RCTTextInputComponentView.mm
++++ b/node_modules/react-native/React/Fabric/Mounting/ComponentViews/TextInput/RCTTextInputComponentView.mm
+@@ -34,6 +34,7 @@ @implementation RCTTextInputComponentView {
+ UIView *_backedTextInputView;
+ NSUInteger _mostRecentEventCount;
+ NSAttributedString *_lastStringStateWasUpdatedWith;
++ NSRegularExpression *_regex;
+
+ /*
+ * UIKit uses either UITextField or UITextView as its UIKit element for . UITextField is for single line
+@@ -224,6 +225,13 @@ - (void)updateProps:(const Props::Shared &)props oldProps:(const Props::Shared &
+ if (newTextInputProps.inputAccessoryViewID != oldTextInputProps.inputAccessoryViewID) {
+ _backedTextInputView.inputAccessoryViewID = RCTNSStringFromString(newTextInputProps.inputAccessoryViewID);
+ }
++
++ if (newTextInputProps.regex != oldTextInputProps.regex) {
++ _regex = [NSRegularExpression regularExpressionWithPattern:RCTNSStringFromString(newTextInputProps.regex)
++ options:0
++ error:nil];
++ }
++
+ [super updateProps:props oldProps:oldProps];
+
+ [self setDefaultInputAccessoryView];
+@@ -359,6 +367,14 @@ - (NSString *)textInputShouldChangeText:(NSString *)text inRange:(NSRange)range
+ }
+ }
+
++ if (_regex) {
++ NSMutableString *newString = [_backedTextInputView.attributedText.string mutableCopy];
++ [newString replaceCharactersInRange:range withString:text];
++ if ([_regex numberOfMatchesInString:newString options:0 range:NSMakeRange(0, newString.length)] == 0) {
++ return nil;
++ }
++ }
++
+ if (props.maxLength) {
+ NSInteger allowedLength = props.maxLength - _backedTextInputView.attributedText.string.length + range.length;
+
+diff --git a/node_modules/react-native/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactTextInputManager.java b/node_modules/react-native/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactTextInputManager.java
+index 2cceb14..8fdc0c1 100644
+--- a/node_modules/react-native/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactTextInputManager.java
++++ b/node_modules/react-native/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactTextInputManager.java
+@@ -824,6 +824,47 @@ public class ReactTextInputManager extends BaseViewManager 0) {
++ LinkedList list = new LinkedList<>();
++ for (InputFilter currentFilter : currentFilters) {
++ if (!(currentFilter instanceof RegexFilter)) {
++ list.add(currentFilter);
++ }
++ }
++ if (!list.isEmpty()) {
++ newFilters = (InputFilter[]) list.toArray(new InputFilter[list.size()]);
++ }
++ }
++ } else {
++ if (currentFilters.length > 0) {
++ newFilters = currentFilters;
++ boolean replaced = false;
++ for (int i = 0; i < currentFilters.length; i++) {
++ if (currentFilters[i] instanceof RegexFilter) {
++ currentFilters[i] = new RegexFilter(regex);
++ replaced = true;
++ }
++ }
++ if (!replaced) {
++ newFilters = new InputFilter[currentFilters.length + 1];
++ System.arraycopy(currentFilters, 0, newFilters, 0, currentFilters.length);
++ newFilters[currentFilters.length] = new RegexFilter(regex);
++ }
++ } else {
++ newFilters = new InputFilter[1];
++ newFilters[0] = new RegexFilter(regex);
++ }
++ }
++
++ view.setFilters(newFilters);
++ }
++
+ @ReactProp(name = "maxLength")
+ public void setMaxLength(ReactEditText view, @Nullable Integer maxLength) {
+ InputFilter[] currentFilters = view.getFilters();
+@@ -854,7 +895,7 @@ public class ReactTextInputManager extends BaseViewManager>;
[ONYXKEYS.NVP_PRIORITY_MODE]: ValueOf;
[ONYXKEYS.NVP_BLOCKED_FROM_CONCIERGE]: OnyxTypes.BlockedFromConcierge;
diff --git a/src/ROUTES.ts b/src/ROUTES.ts
index 2e895537eaac..45501bf46374 100644
--- a/src/ROUTES.ts
+++ b/src/ROUTES.ts
@@ -951,6 +951,10 @@ const ROUTES = {
getRoute: (policyID: string, featureName: string, backTo?: string) =>
getUrlWithBackToParam(`settings/workspaces/${policyID}/upgrade/${encodeURIComponent(featureName)}` as const, backTo),
},
+ WORKSPACE_DOWNGRADE: {
+ route: 'settings/workspaces/:policyID/downgrade/',
+ getRoute: (policyID: string) => `settings/workspaces/${policyID}/downgrade/` as const,
+ },
WORKSPACE_CATEGORIES_SETTINGS: {
route: 'settings/workspaces/:policyID/categories/settings',
getRoute: (policyID: string) => `settings/workspaces/${policyID}/categories/settings` as const,
diff --git a/src/SCREENS.ts b/src/SCREENS.ts
index feded7c81a47..dea0f028e1a0 100644
--- a/src/SCREENS.ts
+++ b/src/SCREENS.ts
@@ -532,6 +532,7 @@ const SCREENS = {
DISTANCE_RATE_TAX_RECLAIMABLE_ON_EDIT: 'Distance_Rate_Tax_Reclaimable_On_Edit',
DISTANCE_RATE_TAX_RATE_EDIT: 'Distance_Rate_Tax_Rate_Edit',
UPGRADE: 'Workspace_Upgrade',
+ DOWNGRADE: 'Workspace_Downgrade',
RULES: 'Policy_Rules',
RULES_CUSTOM_NAME: 'Rules_Custom_Name',
RULES_AUTO_APPROVE_REPORTS_UNDER: 'Rules_Auto_Approve_Reports_Under',
diff --git a/src/components/AccountSwitcher.tsx b/src/components/AccountSwitcher.tsx
index 8ccab44a2cb9..ad58294c0cc8 100644
--- a/src/components/AccountSwitcher.tsx
+++ b/src/components/AccountSwitcher.tsx
@@ -152,7 +152,7 @@ function AccountSwitcher() {
>
{currentUserPersonalDetails?.displayName}
- {canSwitchAccounts && (
+ {!!canSwitchAccounts && (
- {canSwitchAccounts && (
+ {!!canSwitchAccounts && (
{
diff --git a/src/components/AmountForm.tsx b/src/components/AmountForm.tsx
index 4848577bdea0..de3a1fe39829 100644
--- a/src/components/AmountForm.tsx
+++ b/src/components/AmountForm.tsx
@@ -238,6 +238,7 @@ function AmountForm(
forwardDeletePressedRef.current = key === 'delete' || (allowedOS.includes(operatingSystem ?? '') && event.nativeEvent.ctrlKey && key === 'd');
};
+ const regex = useMemo(() => MoneyRequestUtils.amountRegex(decimals, amountMaxLength), [decimals, amountMaxLength]);
const formattedAmount = MoneyRequestUtils.replaceAllDigits(currentAmount, toLocaleDigit);
const canUseTouchScreen = DeviceCapabilities.canUseTouchScreen();
@@ -261,6 +262,7 @@ function AmountForm(
keyboardType={CONST.KEYBOARD_TYPE.DECIMAL_PAD}
inputMode={CONST.INPUT_MODE.DECIMAL}
errorText={errorText}
+ regex={regex}
// eslint-disable-next-line react/jsx-props-no-spreading
{...rest}
/>
@@ -300,6 +302,7 @@ function AmountForm(
isCurrencyPressable={isCurrencyPressable}
style={[styles.iouAmountTextInput]}
containerStyle={[styles.iouAmountTextInputContainer]}
+ regex={regex}
// eslint-disable-next-line react/jsx-props-no-spreading
{...rest}
/>
diff --git a/src/components/AmountTextInput.tsx b/src/components/AmountTextInput.tsx
index 52c32ce1f584..2e0d3e62afa0 100644
--- a/src/components/AmountTextInput.tsx
+++ b/src/components/AmountTextInput.tsx
@@ -39,7 +39,7 @@ type AmountTextInputProps = {
/** Hide the focus styles on TextInput */
hideFocusedState?: boolean;
-} & Pick;
+} & Pick;
function AmountTextInput(
{
diff --git a/src/components/AmountWithoutCurrencyForm.tsx b/src/components/AmountWithoutCurrencyForm.tsx
index 78b7c84ecb54..6a9fc22f68f8 100644
--- a/src/components/AmountWithoutCurrencyForm.tsx
+++ b/src/components/AmountWithoutCurrencyForm.tsx
@@ -1,7 +1,8 @@
-import React, {useCallback, useMemo} from 'react';
+import React, {useCallback, useMemo, useState} from 'react';
import type {ForwardedRef} from 'react';
+import type {NativeSyntheticEvent, TextInputSelectionChangeEventData} from 'react-native';
import useLocalize from '@hooks/useLocalize';
-import {addLeadingZero, replaceAllDigits, replaceCommasWithPeriod, stripSpacesFromAmount, validateAmount} from '@libs/MoneyRequestUtils';
+import {addLeadingZero, amountRegex, replaceAllDigits, replaceCommasWithPeriod, stripSpacesFromAmount, validateAmount} from '@libs/MoneyRequestUtils';
import CONST from '@src/CONST';
import TextInput from './TextInput';
import type {BaseTextInputProps, BaseTextInputRef} from './TextInput/BaseTextInput/types';
@@ -21,6 +22,11 @@ function AmountWithoutCurrencyForm(
const {toLocaleDigit} = useLocalize();
const currentAmount = useMemo(() => (typeof amount === 'string' ? amount : ''), [amount]);
+ const [selection, setSelection] = useState({
+ start: currentAmount.length,
+ end: currentAmount.length,
+ });
+ const decimals = 2;
/**
* Sets the selection and the amount accordingly to the value passed to the input
@@ -33,7 +39,10 @@ function AmountWithoutCurrencyForm(
const newAmountWithoutSpaces = stripSpacesFromAmount(newAmount);
const replacedCommasAmount = replaceCommasWithPeriod(newAmountWithoutSpaces);
const withLeadingZero = addLeadingZero(replacedCommasAmount);
- if (!validateAmount(withLeadingZero, 2)) {
+ if (!validateAmount(withLeadingZero, decimals)) {
+ // Use a shallow copy of selection to trigger setSelection
+ // More info: https://github.com/Expensify/App/issues/16385
+ setSelection((prevSelection) => ({...prevSelection}));
return;
}
onInputChange?.(withLeadingZero);
@@ -41,12 +50,17 @@ function AmountWithoutCurrencyForm(
[onInputChange],
);
+ const regex = useMemo(() => amountRegex(decimals), [decimals]);
const formattedAmount = replaceAllDigits(currentAmount, toLocaleDigit);
return (
) => {
+ setSelection(e.nativeEvent.selection);
+ }}
inputID={inputID}
name={name}
label={label}
@@ -55,6 +69,7 @@ function AmountWithoutCurrencyForm(
role={role}
ref={ref}
keyboardType={CONST.KEYBOARD_TYPE.DECIMAL_PAD}
+ regex={regex}
// eslint-disable-next-line react/jsx-props-no-spreading
{...rest}
/>
diff --git a/src/components/AttachmentPicker/index.native.tsx b/src/components/AttachmentPicker/index.native.tsx
index d3a51c7fc0f0..220be2d61aa6 100644
--- a/src/components/AttachmentPicker/index.native.tsx
+++ b/src/components/AttachmentPicker/index.native.tsx
@@ -16,6 +16,8 @@ import useArrowKeyFocusManager from '@hooks/useArrowKeyFocusManager';
import useKeyboardShortcut from '@hooks/useKeyboardShortcut';
import useLocalize from '@hooks/useLocalize';
import useResponsiveLayout from '@hooks/useResponsiveLayout';
+import useStyleUtils from '@hooks/useStyleUtils';
+import useTheme from '@hooks/useTheme';
import useThemeStyles from '@hooks/useThemeStyles';
import * as FileUtils from '@libs/fileDownload/FileUtils';
import CONST from '@src/CONST';
@@ -33,46 +35,46 @@ type Item = {
pickAttachment: () => Promise;
};
-/**
- * See https://github.com/react-native-image-picker/react-native-image-picker/#options
- * for ImagePicker configuration options
- */
-const imagePickerOptions: Partial = {
- includeBase64: false,
- saveToPhotos: false,
- selectionLimit: 1,
- includeExtra: false,
- assetRepresentationMode: 'current',
-};
-
/**
* Return imagePickerOptions based on the type
*/
-const getImagePickerOptions = (type: string): CameraOptions => {
+const getImagePickerOptions = (type: string, fileLimit: number): CameraOptions | ImageLibraryOptions => {
// mediaType property is one of the ImagePicker configuration to restrict types'
const mediaType = type === CONST.ATTACHMENT_PICKER_TYPE.IMAGE ? 'photo' : 'mixed';
+
+ /**
+ * See https://github.com/react-native-image-picker/react-native-image-picker/#options
+ * for ImagePicker configuration options
+ */
return {
mediaType,
- ...imagePickerOptions,
+ includeBase64: false,
+ saveToPhotos: false,
+ includeExtra: false,
+ assetRepresentationMode: 'current',
+ selectionLimit: fileLimit,
};
};
/**
* Return documentPickerOptions based on the type
* @param {String} type
+ * @param {Number} fileLimit
* @returns {Object}
*/
-const getDocumentPickerOptions = (type: string): DocumentPickerOptions => {
+const getDocumentPickerOptions = (type: string, fileLimit: number): DocumentPickerOptions => {
if (type === CONST.ATTACHMENT_PICKER_TYPE.IMAGE) {
return {
type: [RNDocumentPicker.types.images],
copyTo: 'cachesDirectory',
+ allowMultiSelection: fileLimit !== 1,
};
}
return {
type: [RNDocumentPicker.types.allFiles],
copyTo: 'cachesDirectory',
+ allowMultiSelection: fileLimit !== 1,
};
};
@@ -111,13 +113,16 @@ function AttachmentPicker({
type = CONST.ATTACHMENT_PICKER_TYPE.FILE,
children,
shouldHideCameraOption = false,
- shouldHideGalleryOption = false,
shouldValidateImage = true,
+ shouldHideGalleryOption = false,
+ fileLimit = 1,
}: AttachmentPickerProps) {
const styles = useThemeStyles();
+ const StyleUtils = useStyleUtils();
+ const theme = useTheme();
const [isVisible, setIsVisible] = useState(false);
- const completeAttachmentSelection = useRef<(data: FileObject) => void>(() => {});
+ const completeAttachmentSelection = useRef<(data: FileObject[]) => void>(() => {});
const onModalHide = useRef<() => void>();
const onCanceled = useRef<() => void>(() => {});
const popoverRef = useRef(null);
@@ -143,7 +148,7 @@ function AttachmentPicker({
const showImagePicker = useCallback(
(imagePickerFunc: (options: CameraOptions, callback: Callback) => Promise): Promise =>
new Promise((resolve, reject) => {
- imagePickerFunc(getImagePickerOptions(type), (response: ImagePickerResponse) => {
+ imagePickerFunc(getImagePickerOptions(type, fileLimit), (response: ImagePickerResponse) => {
if (response.didCancel) {
// When the user cancelled resolve with no attachment
return resolve();
@@ -200,7 +205,7 @@ function AttachmentPicker({
}
});
}),
- [showGeneralAlert, type],
+ [fileLimit, showGeneralAlert, type],
);
/**
* Launch the DocumentPicker. Results are in the same format as ImagePicker
@@ -209,7 +214,7 @@ function AttachmentPicker({
*/
const showDocumentPicker = useCallback(
(): Promise =>
- RNDocumentPicker.pick(getDocumentPickerOptions(type)).catch((error: Error) => {
+ RNDocumentPicker.pick(getDocumentPickerOptions(type, fileLimit)).catch((error: Error) => {
if (RNDocumentPicker.isCancel(error)) {
return;
}
@@ -217,7 +222,7 @@ function AttachmentPicker({
showGeneralAlert(error.message);
throw error;
}),
- [showGeneralAlert, type],
+ [fileLimit, showGeneralAlert, type],
);
const menuItemData: Item[] = useMemo(() => {
@@ -261,7 +266,7 @@ function AttachmentPicker({
* @param onPickedHandler A callback that will be called with the selected attachment
* @param onCanceledHandler A callback that will be called without a selected attachment
*/
- const open = (onPickedHandler: (file: FileObject) => void, onCanceledHandler: () => void = () => {}) => {
+ const open = (onPickedHandler: (files: FileObject[]) => void, onCanceledHandler: () => void = () => {}) => {
// eslint-disable-next-line react-compiler/react-compiler
completeAttachmentSelection.current = onPickedHandler;
onCanceled.current = onCanceledHandler;
@@ -286,7 +291,7 @@ function AttachmentPicker({
}
return getDataForUpload(fileData)
.then((result) => {
- completeAttachmentSelection.current(result);
+ completeAttachmentSelection.current([result]);
})
.catch((error: Error) => {
showGeneralAlert(error.message);
@@ -301,63 +306,78 @@ function AttachmentPicker({
* sends the selected attachment to the caller (parent component)
*/
const pickAttachment = useCallback(
- (attachments: Asset[] | DocumentPickerResponse[] | void = []): Promise | undefined => {
+ (attachments: Asset[] | DocumentPickerResponse[] | void = []): Promise | undefined => {
if (!attachments || attachments.length === 0) {
onCanceled.current();
- return Promise.resolve();
+ return Promise.resolve([]);
}
- const fileData = attachments[0];
- if (!fileData) {
- onCanceled.current();
- return Promise.resolve();
- }
- /* eslint-disable @typescript-eslint/prefer-nullish-coalescing */
- const fileDataName = ('fileName' in fileData && fileData.fileName) || ('name' in fileData && fileData.name) || '';
- const fileDataUri = ('fileCopyUri' in fileData && fileData.fileCopyUri) || ('uri' in fileData && fileData.uri) || '';
-
- const fileDataObject: FileResponse = {
- name: fileDataName ?? '',
- uri: fileDataUri,
- size: ('size' in fileData && fileData.size) || ('fileSize' in fileData && fileData.fileSize) || null,
- type: fileData.type ?? '',
- width: ('width' in fileData && fileData.width) || undefined,
- height: ('height' in fileData && fileData.height) || undefined,
- };
+ const filesToProcess = attachments.map((fileData) => {
+ if (!fileData) {
+ onCanceled.current();
+ return Promise.resolve();
+ }
- if (!shouldValidateImage && fileDataName && Str.isImage(fileDataName)) {
- ImageSize.getSize(fileDataUri)
- .then(({width, height}) => {
- fileDataObject.width = width;
- fileDataObject.height = height;
- return fileDataObject;
- })
- .then((file) => {
- getDataForUpload(file)
- .then((result) => {
- completeAttachmentSelection.current(result);
- })
- .catch((error: Error) => {
- showGeneralAlert(error.message);
- throw error;
- });
- });
- return;
- }
- /* eslint-enable @typescript-eslint/prefer-nullish-coalescing */
- if (fileDataName && Str.isImage(fileDataName)) {
- ImageSize.getSize(fileDataUri)
- .then(({width, height}) => {
- fileDataObject.width = width;
- fileDataObject.height = height;
- validateAndCompleteAttachmentSelection(fileDataObject);
- })
- .catch(() => showImageCorruptionAlert());
- } else {
+ /* eslint-disable @typescript-eslint/prefer-nullish-coalescing */
+ const fileDataName = ('fileName' in fileData && fileData.fileName) || ('name' in fileData && fileData.name) || '';
+ const fileDataUri = ('fileCopyUri' in fileData && fileData.fileCopyUri) || ('uri' in fileData && fileData.uri) || '';
+
+ const fileDataObject: FileResponse = {
+ name: fileDataName ?? '',
+ uri: fileDataUri,
+ size: ('size' in fileData && fileData.size) || ('fileSize' in fileData && fileData.fileSize) || null,
+ type: fileData.type ?? '',
+ width: ('width' in fileData && fileData.width) || undefined,
+ height: ('height' in fileData && fileData.height) || undefined,
+ };
+
+ if (!shouldValidateImage && fileDataName && Str.isImage(fileDataName)) {
+ return ImageSize.getSize(fileDataUri)
+ .then(({width, height}) => {
+ fileDataObject.width = width;
+ fileDataObject.height = height;
+ return fileDataObject;
+ })
+ .then((file) => {
+ return getDataForUpload(file)
+ .then((result) => completeAttachmentSelection.current([result]))
+ .catch((error) => {
+ if (error instanceof Error) {
+ showGeneralAlert(error.message);
+ } else {
+ showGeneralAlert('An unknown error occurred');
+ }
+ throw error;
+ });
+ })
+ .catch(() => {
+ showImageCorruptionAlert();
+ });
+ }
+
+ if (fileDataName && Str.isImage(fileDataName)) {
+ return ImageSize.getSize(fileDataUri)
+ .then(({width, height}) => {
+ fileDataObject.width = width;
+ fileDataObject.height = height;
+
+ if (fileDataObject.width <= 0 || fileDataObject.height <= 0) {
+ showImageCorruptionAlert();
+ return Promise.resolve(); // Skip processing this corrupted file
+ }
+
+ return validateAndCompleteAttachmentSelection(fileDataObject);
+ })
+ .catch(() => {
+ showImageCorruptionAlert();
+ });
+ }
return validateAndCompleteAttachmentSelection(fileDataObject);
- }
+ });
+
+ return Promise.all(filesToProcess);
},
- [validateAndCompleteAttachmentSelection, showImageCorruptionAlert, shouldValidateImage, showGeneralAlert],
+ [shouldValidateImage, validateAndCompleteAttachmentSelection, showGeneralAlert, showImageCorruptionAlert],
);
/**
@@ -428,6 +448,7 @@ function AttachmentPicker({
title={translate(item.textTranslationKey)}
onPress={() => selectItem(item)}
focused={focusedIndex === menuIndex}
+ wrapperStyle={StyleUtils.getItemBackgroundColorStyle(false, focusedIndex === menuIndex, theme.activeComponentBG, theme.hoverComponentBG)}
/>
))}
diff --git a/src/components/AttachmentPicker/index.tsx b/src/components/AttachmentPicker/index.tsx
index f3c880fcb835..2484198d3916 100644
--- a/src/components/AttachmentPicker/index.tsx
+++ b/src/components/AttachmentPicker/index.tsx
@@ -1,5 +1,6 @@
import React, {useRef} from 'react';
import type {ValueOf} from 'type-fest';
+import type {FileObject} from '@components/AttachmentModal';
import * as Browser from '@libs/Browser';
import Visibility from '@libs/Visibility';
import CONST from '@src/CONST';
@@ -42,9 +43,9 @@ function getAcceptableFileTypesFromAList(fileTypes: Array(null);
- const onPicked = useRef<(file: File) => void>(() => {});
+ const onPicked = useRef<(files: FileObject[]) => void>(() => {});
const onCanceled = useRef<() => void>(() => {});
return (
@@ -62,7 +63,7 @@ function AttachmentPicker({children, type = CONST.ATTACHMENT_PICKER_TYPE.FILE, a
if (file) {
file.uri = URL.createObjectURL(file);
- onPicked.current(file);
+ onPicked.current([file]);
}
// Cleanup after selecting a file to start from a fresh state
@@ -97,6 +98,7 @@ function AttachmentPicker({children, type = CONST.ATTACHMENT_PICKER_TYPE.FILE, a
);
}}
accept={acceptedFileTypes ? getAcceptableFileTypesFromAList(acceptedFileTypes) : getAcceptableFileTypes(type)}
+ multiple={allowMultiple}
/>
{/* eslint-disable-next-line react-compiler/react-compiler */}
{children({
diff --git a/src/components/AttachmentPicker/types.ts b/src/components/AttachmentPicker/types.ts
index ee9d39aabef3..1e2e65761527 100644
--- a/src/components/AttachmentPicker/types.ts
+++ b/src/components/AttachmentPicker/types.ts
@@ -4,8 +4,8 @@ import type {FileObject} from '@components/AttachmentModal';
import type CONST from '@src/CONST';
type PickerOptions = {
- /** A callback that will be called with the selected attachment. */
- onPicked: (file: FileObject) => void;
+ /** A callback that will be called with the selected attachments. */
+ onPicked: (files: FileObject[]) => void;
/** A callback that will be called without a selected attachment. */
onCanceled?: () => void;
};
@@ -49,6 +49,12 @@ type AttachmentPickerProps = {
/** Whether to validate the image and show the alert or not. */
shouldValidateImage?: boolean;
+
+ /** Allow multiple file selection */
+ allowMultiple?: boolean;
+
+ /** Whether to allow multiple files to be selected. */
+ fileLimit?: number;
};
export default AttachmentPickerProps;
diff --git a/src/components/Attachments/AttachmentCarousel/CarouselItem.tsx b/src/components/Attachments/AttachmentCarousel/CarouselItem.tsx
index 103abb2df1bb..4de43a763231 100644
--- a/src/components/Attachments/AttachmentCarousel/CarouselItem.tsx
+++ b/src/components/Attachments/AttachmentCarousel/CarouselItem.tsx
@@ -86,7 +86,7 @@ function CarouselItem({item, onPress, isFocused, isModalHovered}: CarouselItemPr
/>
- {item.hasBeenFlagged && (
+ {!!item.hasBeenFlagged && (
{({safeAreaPaddingBottomStyle}) => {renderButton([styles.m4, styles.alignSelfCenter])}}
diff --git a/src/components/Attachments/AttachmentView/DefaultAttachmentView/index.tsx b/src/components/Attachments/AttachmentView/DefaultAttachmentView/index.tsx
index 23e13833df64..8f149182d9a6 100644
--- a/src/components/Attachments/AttachmentView/DefaultAttachmentView/index.tsx
+++ b/src/components/Attachments/AttachmentView/DefaultAttachmentView/index.tsx
@@ -47,7 +47,7 @@ function DefaultAttachmentView({fileName = '', shouldShowLoadingSpinnerIcon = fa
{fileName}
- {!shouldShowLoadingSpinnerIcon && shouldShowDownloadIcon && (
+ {!shouldShowLoadingSpinnerIcon && !!shouldShowDownloadIcon && (
- {report && !!title && (
+ {!!report && !!title && (
void;
+ onPicked: (image: FileObject[]) => void;
};
type OpenPicker = (args: OpenPickerParams) => void;
@@ -278,7 +278,7 @@ function AvatarWithImagePicker({
return;
}
openPicker({
- onPicked: showAvatarCropModal,
+ onPicked: (data) => showAvatarCropModal(data.at(0) ?? {}),
});
},
shouldCallAfterModalHide: true,
@@ -324,7 +324,7 @@ function AvatarWithImagePicker({
}
if (isUsingDefaultAvatar) {
openPicker({
- onPicked: showAvatarCropModal,
+ onPicked: (data) => showAvatarCropModal(data.at(0) ?? {}),
});
return;
}
@@ -426,7 +426,7 @@ function AvatarWithImagePicker({
// by the user on Safari.
if (index === 0 && Browser.isSafari()) {
openPicker({
- onPicked: showAvatarCropModal,
+ onPicked: (data) => showAvatarCropModal(data.at(0) ?? {}),
});
}
}}
@@ -443,7 +443,7 @@ function AvatarWithImagePicker({
)}
- {errorData.validationError && (
+ {!!errorData.validationError && (
- {icon && (
+ {!!icon && (
- {shouldShowIcon && icon && (
+ {shouldShowIcon && !!icon && (
(
<>
- {subtitle && (
+ {!!subtitle && (
- {animation && (
+ {!!animation && (
)}
- {icon && (
+ {!!icon && (
- {icon && (
+ {!!icon && (
({
enterKeyEventListenerPriority={enterKeyEventListenerPriority}
/>
)}
- {(shouldAlwaysShowDropdownMenu || options.length > 1) && popoverAnchorPosition && (
+ {(shouldAlwaysShowDropdownMenu || options.length > 1) && !!popoverAnchorPosition && (
{
diff --git a/src/components/CheckboxWithLabel.tsx b/src/components/CheckboxWithLabel.tsx
index db62aa9e1441..0647b495bd33 100644
--- a/src/components/CheckboxWithLabel.tsx
+++ b/src/components/CheckboxWithLabel.tsx
@@ -95,8 +95,8 @@ function CheckboxWithLabel(
style={[styles.flexRow, styles.alignItemsCenter, styles.noSelect, styles.w100]}
wrapperStyle={[styles.ml3, styles.pr2, styles.w100, styles.flexWrap, styles.flexShrink1]}
>
- {label && {label}}
- {LabelComponent && }
+ {!!label && {label}}
+ {!!LabelComponent && }
diff --git a/src/components/CollapsibleSection/index.tsx b/src/components/CollapsibleSection/index.tsx
index d339f005e3d3..3776dfa2cf9b 100644
--- a/src/components/CollapsibleSection/index.tsx
+++ b/src/components/CollapsibleSection/index.tsx
@@ -63,7 +63,7 @@ function CollapsibleSection({title, children, titleStyle, textStyle, wrapperStyl
src={src}
/>
- {shouldShowSectionBorder && }
+ {!!shouldShowSectionBorder && }
{children}
diff --git a/src/components/ConfirmContent.tsx b/src/components/ConfirmContent.tsx
index bda78b9b320d..cb0fc6e8e8cb 100644
--- a/src/components/ConfirmContent.tsx
+++ b/src/components/ConfirmContent.tsx
@@ -168,7 +168,7 @@ function ConfirmContent({
)}
- {iconSource && (
+ {!!iconSource && (
- {title && {titleAlreadyTranslated ?? translate(title)}}
+ {!!title && {titleAlreadyTranslated ?? translate(title)}}
{children}
>
);
diff --git a/src/components/DecisionModal.tsx b/src/components/DecisionModal.tsx
index a9bd0b204d79..927ba1ecab11 100644
--- a/src/components/DecisionModal.tsx
+++ b/src/components/DecisionModal.tsx
@@ -55,7 +55,7 @@ function DecisionModal({title, prompt = '', firstOptionText, secondOptionText, o
{prompt}
- {firstOptionText && (
+ {!!firstOptionText && (
- {isReceiptThumbnail && fileExtension && (
+ {isReceiptThumbnail && !!fileExtension && (
[
- isFocused ? themeStyles.emojiItemKeyboardHighlighted : {},
- isHovered || isHighlighted ? themeStyles.emojiItemHighlighted : {},
+ isFocused || isHovered || isHighlighted ? themeStyles.emojiItemHighlighted : {},
Browser.isMobile() && StyleUtils.getButtonBackgroundColorStyle(getButtonState(false, pressed)),
themeStyles.emojiItem,
]}
diff --git a/src/components/FeatureList.tsx b/src/components/FeatureList.tsx
index fe209e90eb70..70c56ad5d963 100644
--- a/src/components/FeatureList.tsx
+++ b/src/components/FeatureList.tsx
@@ -131,7 +131,7 @@ function FeatureList({
large
/>
)}
- {ctaErrorMessage && (
+ {!!ctaErrorMessage && (
;
diff --git a/src/components/HeaderWithBackButton/index.tsx b/src/components/HeaderWithBackButton/index.tsx
index b71f4db246a8..7eba7a01a610 100755
--- a/src/components/HeaderWithBackButton/index.tsx
+++ b/src/components/HeaderWithBackButton/index.tsx
@@ -173,7 +173,7 @@ function HeaderWithBackButton({
)}
- {icon && (
+ {!!icon && (
)}
- {policyAvatar && (
+ {!!policyAvatar && (
document.removeEventListener('mouseover', unsetHoveredIfOutside);
}, [isHovered, elementRef, shouldFreezeCapture]);
@@ -126,13 +126,13 @@ function ActiveHoverable({onHoverIn, onHoverOut, shouldHandleScroll, shouldFreez
(event: MouseEvent) => {
// Check if the blur event occurred due to clicking outside the element
// and the wrapperView contains the element that caused the blur and reset isHovered
- if (!elementRef.current?.contains(event.target as Node) && !elementRef.current?.contains(event.relatedTarget as Node)) {
+ if (!elementRef.current?.contains(event.target as Node) && !elementRef.current?.contains(event.relatedTarget as Node) && !shouldFreezeCapture) {
setIsHovered(false);
}
onBlur?.(event);
},
- [onBlur],
+ [onBlur, shouldFreezeCapture],
);
const handleAndForwardOnMouseMove = useCallback(
diff --git a/src/components/Icon/Illustrations.ts b/src/components/Icon/Illustrations.ts
index 18ae1792686f..0efb65ed7a61 100644
--- a/src/components/Icon/Illustrations.ts
+++ b/src/components/Icon/Illustrations.ts
@@ -54,6 +54,7 @@ import ThreeLeggedLaptopWoman from '@assets/images/product-illustrations/three_l
import ToddBehindCloud from '@assets/images/product-illustrations/todd-behind-cloud.svg';
import ToddWithPhones from '@assets/images/product-illustrations/todd-with-phones.svg';
import BigVault from '@assets/images/simple-illustrations/emptystate__big-vault.svg';
+import Puzzle from '@assets/images/simple-illustrations/emptystate__puzzlepieces.svg';
import Abacus from '@assets/images/simple-illustrations/simple-illustration__abacus.svg';
import Accounting from '@assets/images/simple-illustrations/simple-illustration__accounting.svg';
import Alert from '@assets/images/simple-illustrations/simple-illustration__alert.svg';
@@ -200,6 +201,7 @@ export {
TrashCan,
TeleScope,
Profile,
+ Puzzle,
PalmTree,
LockClosed,
Gears,
diff --git a/src/components/ImportOnyxState/BaseImportOnyxState.tsx b/src/components/ImportOnyxState/BaseImportOnyxState.tsx
index c6c80d03b58f..3362bc764446 100644
--- a/src/components/ImportOnyxState/BaseImportOnyxState.tsx
+++ b/src/components/ImportOnyxState/BaseImportOnyxState.tsx
@@ -39,7 +39,7 @@ function BaseImportOnyxState({
wrapperStyle={[styles.sectionMenuItemTopDescription]}
onPress={() => {
openPicker({
- onPicked: onFileRead,
+ onPicked: (data) => onFileRead(data.at(0) ?? {}),
});
}}
/>
diff --git a/src/components/InteractiveStepWrapper.tsx b/src/components/InteractiveStepWrapper.tsx
index 6ffe00b9bd5d..290ad628f9cf 100644
--- a/src/components/InteractiveStepWrapper.tsx
+++ b/src/components/InteractiveStepWrapper.tsx
@@ -1,4 +1,5 @@
-import React from 'react';
+import React, {forwardRef} from 'react';
+import type {StyleProp, ViewStyle} from 'react-native';
import {View} from 'react-native';
import useThemeStyles from '@hooks/useThemeStyles';
import CONST from '@src/CONST';
@@ -24,23 +25,57 @@ type InteractiveStepWrapperProps = {
// Array of step names
stepNames?: readonly string[];
+
+ // Should enable max height
+ shouldEnableMaxHeight?: boolean;
+
+ // Should show offline indicator
+ shouldShowOfflineIndicator?: boolean;
+
+ // Should enable picker avoiding
+ shouldEnablePickerAvoiding?: boolean;
+
+ // Call task ID for the guides
+ guidesCallTaskID?: string;
+
+ // Offline indicator style
+ offlineIndicatorStyle?: StyleProp;
};
-function InteractiveStepWrapper({children, wrapperID, handleBackButtonPress, headerTitle, startStepIndex, stepNames}: InteractiveStepWrapperProps) {
+function InteractiveStepWrapper(
+ {
+ children,
+ wrapperID,
+ handleBackButtonPress,
+ headerTitle,
+ startStepIndex,
+ stepNames,
+ shouldEnableMaxHeight,
+ shouldShowOfflineIndicator,
+ shouldEnablePickerAvoiding = false,
+ guidesCallTaskID,
+ offlineIndicatorStyle,
+ }: InteractiveStepWrapperProps,
+ ref: React.ForwardedRef,
+) {
const styles = useThemeStyles();
return (
- {stepNames && (
+ {!!stepNames && (
{}, opti
/>
)}
- {hasDraftComment && optionItem.isAllowedToComment && (
+ {hasDraftComment && !!optionItem.isAllowedToComment && (
{}, opti
/>
)}
- {!shouldShowGreenDotIndicator && !hasBrickError && optionItem.isPinned && (
+ {!shouldShowGreenDotIndicator && !hasBrickError && !!optionItem.isPinned && (
))}
- {errorText && (
+ {!!errorText && (
- {coordinates && (
+ {!!coordinates && (
);
}
diff --git a/src/pages/ReimbursementAccount/PersonalInfo/PersonalInfo.tsx b/src/pages/ReimbursementAccount/PersonalInfo/PersonalInfo.tsx
index 1aa7e519416e..1764bde198af 100644
--- a/src/pages/ReimbursementAccount/PersonalInfo/PersonalInfo.tsx
+++ b/src/pages/ReimbursementAccount/PersonalInfo/PersonalInfo.tsx
@@ -1,38 +1,23 @@
-import type {RefAttributes} from 'react';
import React, {forwardRef, useCallback, useMemo} from 'react';
-import {View} from 'react-native';
-import type {OnyxEntry} from 'react-native-onyx';
-import {withOnyx} from 'react-native-onyx';
-import HeaderWithBackButton from '@components/HeaderWithBackButton';
-import InteractiveStepSubHeader from '@components/InteractiveStepSubHeader';
-import ScreenWrapper from '@components/ScreenWrapper';
+import type {View} from 'react-native';
+import {useOnyx} from 'react-native-onyx';
+import InteractiveStepWrapper from '@components/InteractiveStepWrapper';
import useLocalize from '@hooks/useLocalize';
import useSubStep from '@hooks/useSubStep';
import type {SubStepProps} from '@hooks/useSubStep/types';
-import useThemeStyles from '@hooks/useThemeStyles';
import getInitialSubstepForPersonalInfo from '@pages/ReimbursementAccount/utils/getInitialSubstepForPersonalInfo';
import getSubstepValues from '@pages/ReimbursementAccount/utils/getSubstepValues';
import * as BankAccounts from '@userActions/BankAccounts';
import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
-import type {ReimbursementAccountForm} from '@src/types/form';
import INPUT_IDS from '@src/types/form/ReimbursementAccountForm';
-import type {ReimbursementAccount} from '@src/types/onyx';
import Address from './substeps/Address';
import Confirmation from './substeps/Confirmation';
import DateOfBirth from './substeps/DateOfBirth';
import FullName from './substeps/FullName';
import SocialSecurityNumber from './substeps/SocialSecurityNumber';
-type PersonalInfoOnyxProps = {
- /** Reimbursement account from ONYX */
- reimbursementAccount: OnyxEntry;
-
- /** The draft values of the bank account being setup */
- reimbursementAccountDraft: OnyxEntry;
-};
-
-type PersonalInfoProps = PersonalInfoOnyxProps & {
+type PersonalInfoProps = {
/** Goes to the previous step */
onBackButtonPress: () => void;
};
@@ -40,9 +25,11 @@ type PersonalInfoProps = PersonalInfoOnyxProps & {
const PERSONAL_INFO_STEP_KEYS = INPUT_IDS.PERSONAL_INFO_STEP;
const bodyContent: Array> = [FullName, DateOfBirth, SocialSecurityNumber, Address, Confirmation];
-function PersonalInfo({reimbursementAccount, reimbursementAccountDraft, onBackButtonPress}: PersonalInfoProps, ref: React.ForwardedRef) {
+function PersonalInfo({onBackButtonPress}: PersonalInfoProps, ref: React.ForwardedRef) {
const {translate} = useLocalize();
- const styles = useThemeStyles();
+
+ const [reimbursementAccount] = useOnyx(ONYXKEYS.REIMBURSEMENT_ACCOUNT);
+ const [reimbursementAccountDraft] = useOnyx(ONYXKEYS.FORMS.REIMBURSEMENT_ACCOUNT_FORM_DRAFT);
const policyID = reimbursementAccount?.achData?.policyID ?? '-1';
const values = useMemo(() => getSubstepValues(PERSONAL_INFO_STEP_KEYS, reimbursementAccountDraft, reimbursementAccount), [reimbursementAccount, reimbursementAccountDraft]);
@@ -79,40 +66,25 @@ function PersonalInfo({reimbursementAccount, reimbursementAccountDraft, onBackBu
};
return (
-
-
-
-
-
-
+
);
}
PersonalInfo.displayName = 'PersonalInfo';
-export default withOnyx & PersonalInfoProps, PersonalInfoOnyxProps>({
- // @ts-expect-error: ONYXKEYS.REIMBURSEMENT_ACCOUNT is conflicting with ONYXKEYS.FORMS.REIMBURSEMENT_ACCOUNT_FORM
- reimbursementAccount: {
- key: ONYXKEYS.REIMBURSEMENT_ACCOUNT,
- },
- reimbursementAccountDraft: {
- key: ONYXKEYS.FORMS.REIMBURSEMENT_ACCOUNT_FORM_DRAFT,
- },
-})(forwardRef(PersonalInfo));
+export default forwardRef(PersonalInfo);
diff --git a/src/pages/ReimbursementAccount/PersonalInfo/substeps/Address.tsx b/src/pages/ReimbursementAccount/PersonalInfo/substeps/Address.tsx
index b37dd207ea37..6477c57ac53a 100644
--- a/src/pages/ReimbursementAccount/PersonalInfo/substeps/Address.tsx
+++ b/src/pages/ReimbursementAccount/PersonalInfo/substeps/Address.tsx
@@ -1,27 +1,11 @@
-import React, {useCallback} from 'react';
-import {View} from 'react-native';
-import type {OnyxEntry} from 'react-native-onyx';
-import {withOnyx} from 'react-native-onyx';
-import FormProvider from '@components/Form/FormProvider';
-import type {FormInputErrors, FormOnyxValues} from '@components/Form/types';
-import Text from '@components/Text';
+import React from 'react';
+import {useOnyx} from 'react-native-onyx';
+import AddressStep from '@components/SubStepForms/AddressStep';
import useLocalize from '@hooks/useLocalize';
import useReimbursementAccountStepFormSubmit from '@hooks/useReimbursementAccountStepFormSubmit';
import type {SubStepProps} from '@hooks/useSubStep/types';
-import useThemeStyles from '@hooks/useThemeStyles';
-import * as ValidationUtils from '@libs/ValidationUtils';
-import AddressFormFields from '@pages/ReimbursementAccount/AddressFormFields';
-import HelpLinks from '@pages/ReimbursementAccount/PersonalInfo/HelpLinks';
import ONYXKEYS from '@src/ONYXKEYS';
import INPUT_IDS from '@src/types/form/ReimbursementAccountForm';
-import type {ReimbursementAccount} from '@src/types/onyx';
-
-type AddressOnyxProps = {
- /** Reimbursement account from ONYX */
- reimbursementAccount: OnyxEntry;
-};
-
-type AddressProps = AddressOnyxProps & SubStepProps;
const PERSONAL_INFO_STEP_KEY = INPUT_IDS.PERSONAL_INFO_STEP;
@@ -34,9 +18,10 @@ const INPUT_KEYS = {
const STEP_FIELDS = [PERSONAL_INFO_STEP_KEY.STREET, PERSONAL_INFO_STEP_KEY.CITY, PERSONAL_INFO_STEP_KEY.STATE, PERSONAL_INFO_STEP_KEY.ZIP_CODE];
-function Address({reimbursementAccount, onNext, isEditing}: AddressProps) {
+function Address({onNext, onMove, isEditing}: SubStepProps) {
const {translate} = useLocalize();
- const styles = useThemeStyles();
+
+ const [reimbursementAccount] = useOnyx(ONYXKEYS.REIMBURSEMENT_ACCOUNT);
const defaultValues = {
street: reimbursementAccount?.achData?.[PERSONAL_INFO_STEP_KEY.STREET] ?? '',
@@ -45,23 +30,6 @@ function Address({reimbursementAccount, onNext, isEditing}: AddressProps) {
zipCode: reimbursementAccount?.achData?.[PERSONAL_INFO_STEP_KEY.ZIP_CODE] ?? '',
};
- const validate = useCallback(
- (values: FormOnyxValues): FormInputErrors => {
- const errors = ValidationUtils.getFieldRequiredErrors(values, STEP_FIELDS);
-
- if (values.requestorAddressStreet && !ValidationUtils.isValidAddress(values.requestorAddressStreet)) {
- errors.requestorAddressStreet = translate('bankAccount.error.addressStreet');
- }
-
- if (values.requestorAddressZipCode && !ValidationUtils.isValidZipCode(values.requestorAddressZipCode)) {
- errors.requestorAddressZipCode = translate('bankAccount.error.zipCode');
- }
-
- return errors;
- },
- [translate],
- );
-
const handleSubmit = useReimbursementAccountStepFormSubmit({
fieldIds: STEP_FIELDS,
onNext,
@@ -69,34 +37,21 @@ function Address({reimbursementAccount, onNext, isEditing}: AddressProps) {
});
return (
-
+ isEditing={isEditing}
+ onNext={onNext}
+ onMove={onMove}
formID={ONYXKEYS.FORMS.REIMBURSEMENT_ACCOUNT_FORM}
- submitButtonText={translate(isEditing ? 'common.confirm' : 'common.next')}
- validate={validate}
+ formTitle={translate('personalInfoStep.enterYourAddress')}
+ formPOBoxDisclaimer={translate('common.noPO')}
onSubmit={handleSubmit}
- submitButtonStyles={[styles.mb0]}
- style={[styles.mh5, styles.flexGrow1]}
- >
-
- {translate('personalInfoStep.enterYourAddress')}
- {translate('common.noPO')}
-
-
-
-
+ stepFields={STEP_FIELDS}
+ inputFieldsIDs={INPUT_KEYS}
+ defaultValues={defaultValues}
+ />
);
}
Address.displayName = 'Address';
-export default withOnyx({
- // @ts-expect-error: ONYXKEYS.REIMBURSEMENT_ACCOUNT is conflicting with ONYXKEYS.FORMS.REIMBURSEMENT_ACCOUNT_FORM
- reimbursementAccount: {
- key: ONYXKEYS.REIMBURSEMENT_ACCOUNT,
- },
-})(Address);
+export default Address;
diff --git a/src/pages/ReimbursementAccount/PersonalInfo/substeps/Confirmation.tsx b/src/pages/ReimbursementAccount/PersonalInfo/substeps/Confirmation.tsx
index af1f081cc3da..d882adedd6fb 100644
--- a/src/pages/ReimbursementAccount/PersonalInfo/substeps/Confirmation.tsx
+++ b/src/pages/ReimbursementAccount/PersonalInfo/substeps/Confirmation.tsx
@@ -1,146 +1,77 @@
import React, {useMemo} from 'react';
-import {View} from 'react-native';
-import type {OnyxEntry} from 'react-native-onyx';
-import {withOnyx} from 'react-native-onyx';
-import Button from '@components/Button';
-import DotIndicatorMessage from '@components/DotIndicatorMessage';
-import MenuItemWithTopDescription from '@components/MenuItemWithTopDescription';
-import SafeAreaConsumer from '@components/SafeAreaConsumer';
-import ScrollView from '@components/ScrollView';
-import Text from '@components/Text';
-import TextLink from '@components/TextLink';
+import {useOnyx} from 'react-native-onyx';
+import ConfirmationStep from '@components/SubStepForms/ConfirmationStep';
import useLocalize from '@hooks/useLocalize';
-import useNetwork from '@hooks/useNetwork';
import type {SubStepProps} from '@hooks/useSubStep/types';
-import useThemeStyles from '@hooks/useThemeStyles';
import * as ErrorUtils from '@libs/ErrorUtils';
import getSubstepValues from '@pages/ReimbursementAccount/utils/getSubstepValues';
import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
-import type {ReimbursementAccountForm} from '@src/types/form';
import INPUT_IDS from '@src/types/form/ReimbursementAccountForm';
-import type {ReimbursementAccount} from '@src/types/onyx';
-
-type ConfirmationOnyxProps = {
- /** Reimbursement account from ONYX */
- reimbursementAccount: OnyxEntry;
-
- /** The draft values of the bank account being setup */
- reimbursementAccountDraft: OnyxEntry;
-};
-
-type ConfirmationProps = ConfirmationOnyxProps & SubStepProps;
const PERSONAL_INFO_STEP_KEYS = INPUT_IDS.PERSONAL_INFO_STEP;
const PERSONAL_INFO_STEP_INDEXES = CONST.REIMBURSEMENT_ACCOUNT.SUBSTEP_INDEX.PERSONAL_INFO;
-function Confirmation({reimbursementAccount, reimbursementAccountDraft, onNext, onMove}: ConfirmationProps) {
+function Confirmation({onNext, onMove, isEditing}: SubStepProps) {
const {translate} = useLocalize();
- const styles = useThemeStyles();
- const {isOffline} = useNetwork();
+
+ const [reimbursementAccount] = useOnyx(ONYXKEYS.REIMBURSEMENT_ACCOUNT);
+ const [reimbursementAccountDraft] = useOnyx(ONYXKEYS.FORMS.REIMBURSEMENT_ACCOUNT_FORM_DRAFT);
const isLoading = reimbursementAccount?.isLoading ?? false;
const values = useMemo(() => getSubstepValues(PERSONAL_INFO_STEP_KEYS, reimbursementAccountDraft, reimbursementAccount), [reimbursementAccount, reimbursementAccountDraft]);
const error = ErrorUtils.getLatestErrorMessage(reimbursementAccount ?? {});
- return (
-
- {({safeAreaPaddingBottomStyle}) => (
-
- {translate('personalInfoStep.letsDoubleCheck')}
- {
- onMove(PERSONAL_INFO_STEP_INDEXES.LEGAL_NAME);
- }}
- />
- {
- onMove(PERSONAL_INFO_STEP_INDEXES.DATE_OF_BIRTH);
- }}
- />
- {
- onMove(PERSONAL_INFO_STEP_INDEXES.SSN);
- }}
- />
- {
- onMove(PERSONAL_INFO_STEP_INDEXES.ADDRESS);
- }}
- />
+ const summaryItems = [
+ {
+ description: translate('personalInfoStep.legalName'),
+ title: `${values[PERSONAL_INFO_STEP_KEYS.FIRST_NAME]} ${values[PERSONAL_INFO_STEP_KEYS.LAST_NAME]}`,
+ shouldShowRightIcon: true,
+ onPress: () => {
+ onMove(PERSONAL_INFO_STEP_INDEXES.LEGAL_NAME);
+ },
+ },
+ {
+ description: translate('common.dob'),
+ title: values[PERSONAL_INFO_STEP_KEYS.DOB],
+ shouldShowRightIcon: true,
+ onPress: () => {
+ onMove(PERSONAL_INFO_STEP_INDEXES.DATE_OF_BIRTH);
+ },
+ },
+ {
+ description: translate('personalInfoStep.last4SSN'),
+ title: values[PERSONAL_INFO_STEP_KEYS.SSN_LAST_4],
+ shouldShowRightIcon: true,
+ onPress: () => {
+ onMove(PERSONAL_INFO_STEP_INDEXES.SSN);
+ },
+ },
+ {
+ description: translate('personalInfoStep.address'),
+ title: `${values[PERSONAL_INFO_STEP_KEYS.STREET]}, ${values[PERSONAL_INFO_STEP_KEYS.CITY]}, ${values[PERSONAL_INFO_STEP_KEYS.STATE]} ${values[PERSONAL_INFO_STEP_KEYS.ZIP_CODE]}`,
+ shouldShowRightIcon: true,
+ onPress: () => {
+ onMove(PERSONAL_INFO_STEP_INDEXES.ADDRESS);
+ },
+ },
+ ];
-
- {`${translate('personalInfoStep.byAddingThisBankAccount')} `}
-
- {translate('onfidoStep.facialScan')}
-
- {', '}
-
- {translate('common.privacy')}
-
- {` ${translate('common.and')} `}
-
- {translate('common.termsOfService')}
-
-
-
- {error && error.length > 0 && (
-
- )}
-
-
-
- )}
-
+ return (
+
);
}
Confirmation.displayName = 'Confirmation';
-export default withOnyx({
- // @ts-expect-error: ONYXKEYS.REIMBURSEMENT_ACCOUNT is conflicting with ONYXKEYS.FORMS.REIMBURSEMENT_ACCOUNT_FORM
- reimbursementAccount: {
- key: ONYXKEYS.REIMBURSEMENT_ACCOUNT,
- },
- reimbursementAccountDraft: {
- key: ONYXKEYS.FORMS.REIMBURSEMENT_ACCOUNT_FORM_DRAFT,
- },
-})(Confirmation);
+export default Confirmation;
diff --git a/src/pages/ReimbursementAccount/PersonalInfo/substeps/DateOfBirth.tsx b/src/pages/ReimbursementAccount/PersonalInfo/substeps/DateOfBirth.tsx
index 8c68380d6e55..526181a6cb84 100644
--- a/src/pages/ReimbursementAccount/PersonalInfo/substeps/DateOfBirth.tsx
+++ b/src/pages/ReimbursementAccount/PersonalInfo/substeps/DateOfBirth.tsx
@@ -1,63 +1,23 @@
-import {subYears} from 'date-fns';
-import React, {useCallback} from 'react';
-import type {OnyxEntry} from 'react-native-onyx';
-import {withOnyx} from 'react-native-onyx';
-import DatePicker from '@components/DatePicker';
-import FormProvider from '@components/Form/FormProvider';
-import InputWrapper from '@components/Form/InputWrapper';
-import type {FormInputErrors, FormOnyxValues} from '@components/Form/types';
-import Text from '@components/Text';
+import React from 'react';
+import {useOnyx} from 'react-native-onyx';
+import DateOfBirthStep from '@components/SubStepForms/DateOfBirthStep';
import useLocalize from '@hooks/useLocalize';
import useReimbursementAccountStepFormSubmit from '@hooks/useReimbursementAccountStepFormSubmit';
import type {SubStepProps} from '@hooks/useSubStep/types';
-import useThemeStyles from '@hooks/useThemeStyles';
-import * as ValidationUtils from '@libs/ValidationUtils';
-import HelpLinks from '@pages/ReimbursementAccount/PersonalInfo/HelpLinks';
-import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
-import type {ReimbursementAccountForm} from '@src/types/form';
import INPUT_IDS from '@src/types/form/ReimbursementAccountForm';
-import type {ReimbursementAccount} from '@src/types/onyx';
-
-type DateOfBirthOnyxProps = {
- /** Reimbursement account from ONYX */
- reimbursementAccount: OnyxEntry;
-
- /** The draft values of the bank account being setup */
- reimbursementAccountDraft: OnyxEntry;
-};
-
-type DateOfBirthProps = DateOfBirthOnyxProps & SubStepProps;
const PERSONAL_INFO_DOB_KEY = INPUT_IDS.PERSONAL_INFO_STEP.DOB;
const STEP_FIELDS = [PERSONAL_INFO_DOB_KEY];
-function DateOfBirth({reimbursementAccount, reimbursementAccountDraft, onNext, isEditing}: DateOfBirthProps) {
+function DateOfBirth({onNext, onMove, isEditing}: SubStepProps) {
const {translate} = useLocalize();
- const styles = useThemeStyles();
-
- const validate = useCallback(
- (values: FormOnyxValues): FormInputErrors => {
- const errors = ValidationUtils.getFieldRequiredErrors(values, STEP_FIELDS);
- if (values.dob) {
- if (!ValidationUtils.isValidPastDate(values.dob) || !ValidationUtils.meetsMaximumAgeRequirement(values.dob)) {
- errors.dob = translate('bankAccount.error.dob');
- } else if (!ValidationUtils.meetsMinimumAgeRequirement(values.dob)) {
- errors.dob = translate('bankAccount.error.age');
- }
- }
-
- return errors;
- },
- [translate],
- );
+ const [reimbursementAccount] = useOnyx(ONYXKEYS.REIMBURSEMENT_ACCOUNT);
+ const [reimbursementAccountDraft] = useOnyx(ONYXKEYS.FORMS.REIMBURSEMENT_ACCOUNT_FORM_DRAFT);
const dobDefaultValue = reimbursementAccount?.achData?.[PERSONAL_INFO_DOB_KEY] ?? reimbursementAccountDraft?.[PERSONAL_INFO_DOB_KEY] ?? '';
- const minDate = subYears(new Date(), CONST.DATE_BIRTH.MAX_AGE);
- const maxDate = subYears(new Date(), CONST.DATE_BIRTH.MIN_AGE_FOR_PAYMENT);
-
const handleSubmit = useReimbursementAccountStepFormSubmit({
fieldIds: STEP_FIELDS,
onNext,
@@ -65,38 +25,20 @@ function DateOfBirth({reimbursementAccount, reimbursementAccountDraft, onNext, i
});
return (
-
+ isEditing={isEditing}
+ onNext={onNext}
+ onMove={onMove}
formID={ONYXKEYS.FORMS.REIMBURSEMENT_ACCOUNT_FORM}
- submitButtonText={translate(isEditing ? 'common.confirm' : 'common.next')}
- validate={validate}
+ formTitle={translate('personalInfoStep.enterYourDateOfBirth')}
onSubmit={handleSubmit}
- style={[styles.mh5, styles.flexGrow2, styles.justifyContentBetween]}
- submitButtonStyles={[styles.mb0]}
- >
- {translate('personalInfoStep.enterYourDateOfBirth')}
-
-
-
+ stepFields={STEP_FIELDS}
+ dobInputID={PERSONAL_INFO_DOB_KEY}
+ dobDefaultValue={dobDefaultValue}
+ />
);
}
DateOfBirth.displayName = 'DateOfBirth';
-export default withOnyx({
- // @ts-expect-error: ONYXKEYS.REIMBURSEMENT_ACCOUNT is conflicting with ONYXKEYS.FORMS.REIMBURSEMENT_ACCOUNT_FORM
- reimbursementAccount: {
- key: ONYXKEYS.REIMBURSEMENT_ACCOUNT,
- },
- reimbursementAccountDraft: {
- key: ONYXKEYS.FORMS.REIMBURSEMENT_ACCOUNT_FORM_DRAFT,
- },
-})(DateOfBirth);
+export default DateOfBirth;
diff --git a/src/pages/ReimbursementAccount/PersonalInfo/substeps/FullName.tsx b/src/pages/ReimbursementAccount/PersonalInfo/substeps/FullName.tsx
index 15c4114432da..8d07b9af7994 100644
--- a/src/pages/ReimbursementAccount/PersonalInfo/substeps/FullName.tsx
+++ b/src/pages/ReimbursementAccount/PersonalInfo/substeps/FullName.tsx
@@ -1,56 +1,25 @@
-import React, {useCallback} from 'react';
-import {View} from 'react-native';
-import type {OnyxEntry} from 'react-native-onyx';
-import {withOnyx} 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 Text from '@components/Text';
-import TextInput from '@components/TextInput';
+import React from 'react';
+import {useOnyx} from 'react-native-onyx';
+import FullNameStep from '@components/SubStepForms/FullNameStep';
import useLocalize from '@hooks/useLocalize';
import useReimbursementAccountStepFormSubmit from '@hooks/useReimbursementAccountStepFormSubmit';
import type {SubStepProps} from '@hooks/useSubStep/types';
-import useThemeStyles from '@hooks/useThemeStyles';
-import * as ValidationUtils from '@libs/ValidationUtils';
-import HelpLinks from '@pages/ReimbursementAccount/PersonalInfo/HelpLinks';
-import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
import INPUT_IDS from '@src/types/form/ReimbursementAccountForm';
-import type {ReimbursementAccount} from '@src/types/onyx';
-
-type FullNameOnyxProps = {
- /** Reimbursement account from ONYX */
- reimbursementAccount: OnyxEntry;
-};
-
-type FullNameProps = FullNameOnyxProps & SubStepProps;
const PERSONAL_INFO_STEP_KEY = INPUT_IDS.PERSONAL_INFO_STEP;
const STEP_FIELDS = [PERSONAL_INFO_STEP_KEY.FIRST_NAME, PERSONAL_INFO_STEP_KEY.LAST_NAME];
-function FullName({reimbursementAccount, onNext, isEditing}: FullNameProps) {
+
+function FullName({onNext, onMove, isEditing}: SubStepProps) {
const {translate} = useLocalize();
- const styles = useThemeStyles();
+
+ const [reimbursementAccount] = useOnyx(ONYXKEYS.REIMBURSEMENT_ACCOUNT);
const defaultValues = {
firstName: reimbursementAccount?.achData?.[PERSONAL_INFO_STEP_KEY.FIRST_NAME] ?? '',
lastName: reimbursementAccount?.achData?.[PERSONAL_INFO_STEP_KEY.LAST_NAME] ?? '',
};
- const validate = useCallback(
- (values: FormOnyxValues): FormInputErrors => {
- const errors = ValidationUtils.getFieldRequiredErrors(values, STEP_FIELDS);
- if (values.firstName && !ValidationUtils.isValidPersonName(values.firstName)) {
- errors.firstName = translate('bankAccount.error.firstName');
- }
-
- if (values.lastName && !ValidationUtils.isValidPersonName(values.lastName)) {
- errors.lastName = translate('bankAccount.error.lastName');
- }
- return errors;
- },
- [translate],
- );
-
const handleSubmit = useReimbursementAccountStepFormSubmit({
fieldIds: STEP_FIELDS,
onNext,
@@ -58,49 +27,21 @@ function FullName({reimbursementAccount, onNext, isEditing}: FullNameProps) {
});
return (
-
+ isEditing={isEditing}
+ onNext={onNext}
+ onMove={onMove}
formID={ONYXKEYS.FORMS.REIMBURSEMENT_ACCOUNT_FORM}
- submitButtonText={translate(isEditing ? 'common.confirm' : 'common.next')}
- validate={validate}
+ formTitle={translate('personalInfoStep.enterYourLegalFirstAndLast')}
onSubmit={handleSubmit}
- style={[styles.mh5, styles.flexGrow1]}
- submitButtonStyles={[styles.mb0]}
- >
-
- {translate('personalInfoStep.enterYourLegalFirstAndLast')}
-
-
-
-
-
-
-
-
-
+ stepFields={STEP_FIELDS}
+ firstNameInputID={PERSONAL_INFO_STEP_KEY.FIRST_NAME}
+ lastNameInputID={PERSONAL_INFO_STEP_KEY.LAST_NAME}
+ defaultValues={defaultValues}
+ />
);
}
FullName.displayName = 'FullName';
-export default withOnyx({
- // @ts-expect-error: ONYXKEYS.REIMBURSEMENT_ACCOUNT is conflicting with ONYXKEYS.FORMS.REIMBURSEMENT_ACCOUNT_FORM
- reimbursementAccount: {
- key: ONYXKEYS.REIMBURSEMENT_ACCOUNT,
- },
-})(FullName);
+export default FullName;
diff --git a/src/pages/ReimbursementAccount/PersonalInfo/substeps/SocialSecurityNumber.tsx b/src/pages/ReimbursementAccount/PersonalInfo/substeps/SocialSecurityNumber.tsx
index 2f08980f2bd0..f94ceef07d2f 100644
--- a/src/pages/ReimbursementAccount/PersonalInfo/substeps/SocialSecurityNumber.tsx
+++ b/src/pages/ReimbursementAccount/PersonalInfo/substeps/SocialSecurityNumber.tsx
@@ -1,36 +1,22 @@
import React, {useCallback} from 'react';
-import {View} from 'react-native';
-import type {OnyxEntry} from 'react-native-onyx';
-import {withOnyx} from 'react-native-onyx';
-import FormProvider from '@components/Form/FormProvider';
-import InputWrapper from '@components/Form/InputWrapper';
+import {useOnyx} from 'react-native-onyx';
import type {FormInputErrors, FormOnyxValues} from '@components/Form/types';
-import Text from '@components/Text';
-import TextInput from '@components/TextInput';
+import SingleFieldStep from '@components/SubStepForms/SingleFieldStep';
import useLocalize from '@hooks/useLocalize';
import useReimbursementAccountStepFormSubmit from '@hooks/useReimbursementAccountStepFormSubmit';
import type {SubStepProps} from '@hooks/useSubStep/types';
-import useThemeStyles from '@hooks/useThemeStyles';
import * as ValidationUtils from '@libs/ValidationUtils';
-import HelpLinks from '@pages/ReimbursementAccount/PersonalInfo/HelpLinks';
import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
import INPUT_IDS from '@src/types/form/ReimbursementAccountForm';
-import type {ReimbursementAccount} from '@src/types/onyx';
-
-type SocialSecurityNumberOnyxProps = {
- /** Reimbursement account from ONYX */
- reimbursementAccount: OnyxEntry;
-};
-
-type SocialSecurityNumberProps = SocialSecurityNumberOnyxProps & SubStepProps;
const PERSONAL_INFO_STEP_KEY = INPUT_IDS.PERSONAL_INFO_STEP;
const STEP_FIELDS = [PERSONAL_INFO_STEP_KEY.SSN_LAST_4];
-function SocialSecurityNumber({reimbursementAccount, onNext, isEditing}: SocialSecurityNumberProps) {
+function SocialSecurityNumber({onNext, onMove, isEditing}: SubStepProps) {
const {translate} = useLocalize();
- const styles = useThemeStyles();
+
+ const [reimbursementAccount] = useOnyx(ONYXKEYS.REIMBURSEMENT_ACCOUNT);
const defaultSsnLast4 = reimbursementAccount?.achData?.[PERSONAL_INFO_STEP_KEY.SSN_LAST_4] ?? '';
@@ -54,42 +40,24 @@ function SocialSecurityNumber({reimbursementAccount, onNext, isEditing}: SocialS
});
return (
-
+ isEditing={isEditing}
+ onNext={onNext}
+ onMove={onMove}
formID={ONYXKEYS.FORMS.REIMBURSEMENT_ACCOUNT_FORM}
- submitButtonText={translate(isEditing ? 'common.confirm' : 'common.next')}
+ formTitle={translate('personalInfoStep.enterTheLast4')}
+ formDisclaimer={translate('personalInfoStep.dontWorry')}
validate={validate}
onSubmit={handleSubmit}
- style={[styles.mh5, styles.flexGrow1]}
- submitButtonStyles={[styles.mb0]}
- >
-
- {translate('personalInfoStep.enterTheLast4')}
- {translate('personalInfoStep.dontWorry')}
-
-
-
-
-
-
+ inputId={PERSONAL_INFO_STEP_KEY.SSN_LAST_4}
+ inputLabel={translate('personalInfoStep.last4SSN')}
+ inputMode={CONST.INPUT_MODE.NUMERIC}
+ defaultValue={defaultSsnLast4}
+ maxLength={CONST.BANK_ACCOUNT.MAX_LENGTH.SSN}
+ />
);
}
SocialSecurityNumber.displayName = 'SocialSecurityNumber';
-export default withOnyx({
- // @ts-expect-error: ONYXKEYS.REIMBURSEMENT_ACCOUNT is conflicting with ONYXKEYS.FORMS.REIMBURSEMENT_ACCOUNT_FORM
- reimbursementAccount: {
- key: ONYXKEYS.REIMBURSEMENT_ACCOUNT,
- },
-})(SocialSecurityNumber);
+export default SocialSecurityNumber;
diff --git a/src/pages/ReimbursementAccount/ReimbursementAccountPage.tsx b/src/pages/ReimbursementAccount/ReimbursementAccountPage.tsx
index 04d5173e9c16..9fdcea824c3c 100644
--- a/src/pages/ReimbursementAccount/ReimbursementAccountPage.tsx
+++ b/src/pages/ReimbursementAccount/ReimbursementAccountPage.tsx
@@ -2,7 +2,7 @@ import type {RouteProp} from '@react-navigation/native';
import type {StackScreenProps} from '@react-navigation/stack';
import {Str} from 'expensify-common';
import lodashPick from 'lodash/pick';
-import React, {useEffect, useRef, useState} from 'react';
+import React, {useCallback, useEffect, useRef, useState} from 'react';
import {View} from 'react-native';
import {useOnyx} from 'react-native-onyx';
import type {ValueOf} from 'type-fest';
@@ -177,7 +177,6 @@ function ReimbursementAccountPage({route, policy}: ReimbursementAccountPageProps
which acts similarly to `componentDidUpdate` when the `reimbursementAccount` dependency changes.
*/
const [hasACHDataBeenLoaded, setHasACHDataBeenLoaded] = useState(reimbursementAccount !== CONST.REIMBURSEMENT_ACCOUNT.DEFAULT_DATA && isPreviousPolicy);
- const [shouldShowContinueSetupButton, setShouldShowContinueSetupButton] = useState(getShouldShowContinueSetupButtonInitialValue());
function getBankAccountFields(fieldNames: InputID[]): Partial {
return {
@@ -188,21 +187,28 @@ function ReimbursementAccountPage({route, policy}: ReimbursementAccountPageProps
/**
* Returns true if a VBBA exists in any state other than OPEN or LOCKED
*/
- function hasInProgressVBBA(): boolean {
+ const hasInProgressVBBA = useCallback(() => {
return !!achData?.bankAccountID && !!achData?.state && achData?.state !== BankAccount.STATE.OPEN && achData?.state !== BankAccount.STATE.LOCKED;
- }
+ }, [achData?.bankAccountID, achData?.state]);
/*
* Calculates the state used to show the "Continue with setup" view. If a bank account setup is already in progress and
* no specific further step was passed in the url we'll show the workspace bank account reset modal if the user wishes to start over
*/
- function getShouldShowContinueSetupButtonInitialValue(): boolean {
+ const getShouldShowContinueSetupButtonInitialValue = useCallback(() => {
if (!hasInProgressVBBA()) {
// Since there is no VBBA in progress, we won't need to show the component ContinueBankAccountSetup
return false;
}
return achData?.state === BankAccount.STATE.PENDING || [CONST.BANK_ACCOUNT.STEP.BANK_ACCOUNT, ''].includes(getStepToOpenFromRouteParams(route));
- }
+ }, [achData?.state, hasInProgressVBBA, route]);
+
+ const [shouldShowContinueSetupButton, setShouldShowContinueSetupButton] = useState(getShouldShowContinueSetupButtonInitialValue());
+
+ useEffect(() => {
+ setShouldShowContinueSetupButton(getShouldShowContinueSetupButtonInitialValue());
+ setHasACHDataBeenLoaded(reimbursementAccount !== CONST.REIMBURSEMENT_ACCOUNT.DEFAULT_DATA && isPreviousPolicy);
+ }, [achData, getShouldShowContinueSetupButtonInitialValue, isPreviousPolicy, reimbursementAccount]);
const handleNextNonUSDBankAccountStep = () => {
switch (nonUSDBankAccountStep) {
diff --git a/src/pages/ReimbursementAccount/VerifyIdentity/VerifyIdentity.tsx b/src/pages/ReimbursementAccount/VerifyIdentity/VerifyIdentity.tsx
index cabaf543a756..943f04d66840 100644
--- a/src/pages/ReimbursementAccount/VerifyIdentity/VerifyIdentity.tsx
+++ b/src/pages/ReimbursementAccount/VerifyIdentity/VerifyIdentity.tsx
@@ -1,13 +1,9 @@
import React, {useCallback} from 'react';
-import {View} from 'react-native';
-import type {OnyxEntry} from 'react-native-onyx';
-import {withOnyx} from 'react-native-onyx';
+import {useOnyx} from 'react-native-onyx';
import FullPageOfflineBlockingView from '@components/BlockingViews/FullPageOfflineBlockingView';
-import HeaderWithBackButton from '@components/HeaderWithBackButton';
-import InteractiveStepSubHeader from '@components/InteractiveStepSubHeader';
+import InteractiveStepWrapper from '@components/InteractiveStepWrapper';
import Onfido from '@components/Onfido';
import type {OnfidoData} from '@components/Onfido/types';
-import ScreenWrapper from '@components/ScreenWrapper';
import ScrollView from '@components/ScrollView';
import useLocalize from '@hooks/useLocalize';
import useThemeStyles from '@hooks/useThemeStyles';
@@ -15,30 +11,22 @@ import Growl from '@libs/Growl';
import * as BankAccounts from '@userActions/BankAccounts';
import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
-import type {ReimbursementAccount} from '@src/types/onyx';
-type VerifyIdentityOnyxProps = {
- /** Reimbursement account from ONYX */
- reimbursementAccount: OnyxEntry;
-
- /** Onfido applicant ID from ONYX */
- onfidoApplicantID: OnyxEntry;
-
- /** The token required to initialize the Onfido SDK */
- onfidoToken: OnyxEntry;
-};
-
-type VerifyIdentityProps = VerifyIdentityOnyxProps & {
+type VerifyIdentityProps = {
/** Goes to the previous step */
onBackButtonPress: () => void;
};
const ONFIDO_ERROR_DISPLAY_DURATION = 10000;
-function VerifyIdentity({reimbursementAccount, onBackButtonPress, onfidoApplicantID, onfidoToken}: VerifyIdentityProps) {
+function VerifyIdentity({onBackButtonPress}: VerifyIdentityProps) {
const styles = useThemeStyles();
const {translate} = useLocalize();
+ const [reimbursementAccount] = useOnyx(ONYXKEYS.REIMBURSEMENT_ACCOUNT);
+ const [onfidoApplicantID] = useOnyx(ONYXKEYS.ONFIDO_APPLICANT_ID);
+ const [onfidoToken] = useOnyx(ONYXKEYS.ONFIDO_TOKEN);
+
const policyID = reimbursementAccount?.achData?.policyID ?? '-1';
const handleOnfidoSuccess = useCallback(
(onfidoData: OnfidoData) => {
@@ -61,17 +49,13 @@ function VerifyIdentity({reimbursementAccount, onBackButtonPress, onfidoApplican
};
return (
-
-
-
-
-
+
-
+
);
}
VerifyIdentity.displayName = 'VerifyIdentity';
-export default withOnyx({
- // @ts-expect-error: ONYXKEYS.REIMBURSEMENT_ACCOUNT is conflicting with ONYXKEYS.FORMS.REIMBURSEMENT_ACCOUNT_FORM
- reimbursementAccount: {
- key: ONYXKEYS.REIMBURSEMENT_ACCOUNT,
- },
- onfidoApplicantID: {
- key: ONYXKEYS.ONFIDO_APPLICANT_ID,
- },
- onfidoToken: {
- key: ONYXKEYS.ONFIDO_TOKEN,
- },
-})(VerifyIdentity);
+export default VerifyIdentity;
diff --git a/src/pages/ReportDetailsPage.tsx b/src/pages/ReportDetailsPage.tsx
index 7de12eeda892..9ec3691f49a8 100644
--- a/src/pages/ReportDetailsPage.tsx
+++ b/src/pages/ReportDetailsPage.tsx
@@ -726,7 +726,7 @@ function ReportDetailsPage({policies, report, route}: ReportDetailsPageProps) {
/>
);
- const nameSectionTitleField = titleField && (
+ const nameSectionTitleField = !!titleField && (
() => {
- UserSearchPhraseActions.clearUserSearchPhrase();
- },
- [],
- );
-
- useEffect(() => {
- UserSearchPhraseActions.updateUserSearchPhrase(debouncedSearchValue);
- }, [debouncedSearchValue]);
+ const [searchValue, setSearchValue] = useState('');
useEffect(() => {
if (isFocused) {
@@ -424,9 +412,7 @@ function ReportParticipantsPage({report, route}: ReportParticipantsPageProps) {
shouldShowTextInput={shouldShowTextInput}
textInputLabel={translate('selectionList.findMember')}
textInputValue={searchValue}
- onChangeText={(value) => {
- setSearchValue(value);
- }}
+ onChangeText={setSearchValue}
headerMessage={headerMessage}
ListItem={TableListItem}
onSelectRow={openMemberDetails}
diff --git a/src/pages/RoomDescriptionPage.tsx b/src/pages/RoomDescriptionPage.tsx
index a70f17d1468c..fde0eb72e2dc 100644
--- a/src/pages/RoomDescriptionPage.tsx
+++ b/src/pages/RoomDescriptionPage.tsx
@@ -40,7 +40,7 @@ function RoomDescriptionPage({report, policies}: RoomDescriptionPageProps) {
const route = useRoute>();
const backTo = route.params.backTo;
const styles = useThemeStyles();
- const [description, setDescription] = useState(() => Parser.htmlToMarkdown(report?.description ?? ''));
+ const [description, setDescription] = useState(() => Parser.htmlToMarkdown(ReportUtils.getReportDescription(report)));
const reportDescriptionInputRef = useRef(null);
const focusTimeoutRef = useRef | null>(null);
const {translate} = useLocalize();
diff --git a/src/pages/RoomMembersPage.tsx b/src/pages/RoomMembersPage.tsx
index 1018b86083be..6a89eca6f778 100644
--- a/src/pages/RoomMembersPage.tsx
+++ b/src/pages/RoomMembersPage.tsx
@@ -19,7 +19,6 @@ import SelectionListWithModal from '@components/SelectionListWithModal';
import Text from '@components/Text';
import type {WithCurrentUserPersonalDetailsProps} from '@components/withCurrentUserPersonalDetails';
import withCurrentUserPersonalDetails from '@components/withCurrentUserPersonalDetails';
-import useDebouncedState from '@hooks/useDebouncedState';
import useLocalize from '@hooks/useLocalize';
import useNetwork from '@hooks/useNetwork';
import useResponsiveLayout from '@hooks/useResponsiveLayout';
@@ -55,7 +54,7 @@ function RoomMembersPage({report, policies}: RoomMembersPageProps) {
const [selectedMembers, setSelectedMembers] = useState([]);
const [removeMembersConfirmModalVisible, setRemoveMembersConfirmModalVisible] = useState(false);
const [userSearchPhrase] = useOnyx(ONYXKEYS.ROOM_MEMBERS_USER_SEARCH_PHRASE);
- const [searchValue, debouncedSearchTerm, setSearchValue] = useDebouncedState('');
+ const [searchValue, setSearchValue] = useState('');
const [didLoadRoomMembers, setDidLoadRoomMembers] = useState(false);
const personalDetails = usePersonalDetails() || CONST.EMPTY_OBJECT;
const policy = useMemo(() => policies?.[`${ONYXKEYS.COLLECTION.POLICY}${report?.policyID ?? ''}`], [policies, report?.policyID]);
@@ -71,14 +70,6 @@ function RoomMembersPage({report, policies}: RoomMembersPageProps) {
const [selectionMode] = useOnyx(ONYXKEYS.MOBILE_SELECTION_MODE);
const canSelectMultiple = isSmallScreenWidth ? selectionMode?.isEnabled : true;
- useEffect(() => {
- setSearchValue(userSearchPhrase ?? '');
- }, [isFocusedScreen, setSearchValue, userSearchPhrase]);
-
- useEffect(() => {
- UserSearchPhraseActions.updateUserSearchPhrase(debouncedSearchTerm);
- }, [debouncedSearchTerm]);
-
useEffect(() => {
if (isFocusedScreen) {
return;
@@ -195,6 +186,17 @@ function RoomMembersPage({report, policies}: RoomMembersPageProps) {
return activeParticipants.length >= CONST.SHOULD_SHOW_MEMBERS_SEARCH_INPUT_BREAKPOINT;
}, [participants, personalDetails, isOffline, report]);
+ useEffect(() => {
+ if (!isFocusedScreen || !shouldShowTextInput) {
+ return;
+ }
+ setSearchValue(userSearchPhrase ?? '');
+ }, [isFocusedScreen, shouldShowTextInput, userSearchPhrase]);
+
+ useEffect(() => {
+ UserSearchPhraseActions.updateUserSearchPhrase(searchValue);
+ }, [searchValue]);
+
useEffect(() => {
if (!isFocusedScreen) {
return;
@@ -385,9 +387,7 @@ function RoomMembersPage({report, policies}: RoomMembersPageProps) {
textInputLabel={translate('selectionList.findMember')}
disableKeyboardShortcuts={removeMembersConfirmModalVisible}
textInputValue={searchValue}
- onChangeText={(value) => {
- setSearchValue(value);
- }}
+ onChangeText={setSearchValue}
headerMessage={headerMessage}
turnOnSelectionModeOnLongPress
onTurnOnSelectionMode={(item) => item && toggleUser(item)}
diff --git a/src/pages/Search/AdvancedSearchFilters.tsx b/src/pages/Search/AdvancedSearchFilters.tsx
index 286ec2917de6..ce4daabc983a 100644
--- a/src/pages/Search/AdvancedSearchFilters.tsx
+++ b/src/pages/Search/AdvancedSearchFilters.tsx
@@ -361,7 +361,7 @@ function AdvancedSearchFilters() {
})}
- {displaySearchButton && (
+ {!!displaySearchButton && (
- {ctaErrorMessage && (
+ {!!ctaErrorMessage && (
- {queryJSON && (
+ {!!queryJSON && (
<>
- {queryJSON && (
+ {!!queryJSON && (
);
}
+ const shouldShowSavedSearchesMenuItemTitle = Object.values(savedSearches ?? {}).filter((s) => s.pendingAction !== CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE || isOffline).length > 0;
return (
<>
@@ -261,7 +264,7 @@ function SearchTypeMenu({queryJSON, searchName}: SearchTypeMenuProps) {
);
})}
- {savedSearches && Object.keys(savedSearches).length > 0 && (
+ {shouldShowSavedSearchesMenuItemTitle && (
<>
{translate('search.savedSearchesMenuItemTitle')}
{autoAuthStateWithDefault === CONST.AUTO_AUTH_STATE.FAILED && }
- {autoAuthStateWithDefault === CONST.AUTO_AUTH_STATE.JUST_SIGNED_IN && is2FARequired && !isSignedIn && login && }
- {autoAuthStateWithDefault === CONST.AUTO_AUTH_STATE.JUST_SIGNED_IN && isSignedIn && !exitTo && login && }
+ {autoAuthStateWithDefault === CONST.AUTO_AUTH_STATE.JUST_SIGNED_IN && is2FARequired && !isSignedIn && !!login && }
+ {autoAuthStateWithDefault === CONST.AUTO_AUTH_STATE.JUST_SIGNED_IN && isSignedIn && !exitTo && !!login && }
{/* If session.autoAuthState isn't available yet, we use shouldStartSignInWithValidateCode to conditionally render the component instead of local autoAuthState which contains a default value of NOT_STARTED */}
{(!autoAuthState ? !shouldStartSignInWithValidateCode : autoAuthStateWithDefault === CONST.AUTO_AUTH_STATE.NOT_STARTED) && !exitTo && (
{headerView}
- {report && ReportUtils.isTaskReport(report) && shouldUseNarrowLayout && ReportUtils.isOpenTaskReport(report, parentReportAction) && (
+ {!!report && ReportUtils.isTaskReport(report) && shouldUseNarrowLayout && ReportUtils.isOpenTaskReport(report, parentReportAction) && (
@@ -756,7 +756,7 @@ function ReportScreen({route, currentReportID = '', navigation}: ReportScreenPro
style={[styles.flex1, styles.justifyContentEnd, styles.overflowHidden]}
testID="report-actions-view-wrapper"
>
- {!shouldShowSkeleton && report && (
+ {!shouldShowSkeleton && !!report && (
- {logo && (
+ {!!logo && (
)}
- {publisher && (
+ {!!publisher && (
)}
- {title && url && (
+ {!!title && !!url && (
)}
- {description && {description}}
- {image?.type && IMAGE_TYPES.includes(image.type) && image.width && image.height && (
+ {!!description && {description}}
+ {!!image?.type && IMAGE_TYPES.includes(image.type) && !!image.width && !!image.height && (
{
onTriggerAttachmentPicker();
openPicker({
- onPicked: displayFileInModal,
+ onPicked: (data) => displayFileInModal(data.at(0) ?? {}),
onCanceled: onCanceledAttachmentPicker,
});
};
diff --git a/src/pages/home/report/ReportActionCompose/SuggestionMention.tsx b/src/pages/home/report/ReportActionCompose/SuggestionMention.tsx
index 7a7230fef333..6a62201058e8 100644
--- a/src/pages/home/report/ReportActionCompose/SuggestionMention.tsx
+++ b/src/pages/home/report/ReportActionCompose/SuggestionMention.tsx
@@ -157,9 +157,9 @@ function SuggestionMention(
useCallback(() => {
const foundSuggestionsCount = suggestionValues.suggestedMentions.length;
if (suggestionValues.prefixType === '#' && foundSuggestionsCount < 5 && isGroupPolicyReport) {
- ReportUserActions.searchInServer(value, policyID);
+ ReportUserActions.searchInServer(suggestionValues.mentionPrefix, policyID);
}
- }, [suggestionValues.suggestedMentions.length, suggestionValues.prefixType, policyID, value, isGroupPolicyReport]),
+ }, [suggestionValues.suggestedMentions.length, suggestionValues.prefixType, suggestionValues.mentionPrefix, policyID, isGroupPolicyReport]),
CONST.TIMING.SEARCH_OPTION_LIST_DEBOUNCE_TIME,
);
diff --git a/src/pages/home/report/ReportActionItem.tsx b/src/pages/home/report/ReportActionItem.tsx
index 0af9bd61120d..a0e2f65a89a0 100644
--- a/src/pages/home/report/ReportActionItem.tsx
+++ b/src/pages/home/report/ReportActionItem.tsx
@@ -961,6 +961,7 @@ function ReportActionItem({
{(hovered) => (
diff --git a/src/pages/home/report/ReportActionItemSingle.tsx b/src/pages/home/report/ReportActionItemSingle.tsx
index 5e1f0a591532..fa8230640c8e 100644
--- a/src/pages/home/report/ReportActionItemSingle.tsx
+++ b/src/pages/home/report/ReportActionItemSingle.tsx
@@ -85,7 +85,9 @@ function ReportActionItemSingle({
const personalDetails = usePersonalDetails() ?? CONST.EMPTY_OBJECT;
const policy = usePolicy(report?.policyID);
const delegatePersonalDetails = personalDetails[action?.delegateAccountID ?? ''];
- const actorAccountID = ReportUtils.getReportActionActorAccountID(action, iouReport);
+ const ownerAccountID = iouReport?.ownerAccountID ?? action?.childOwnerAccountID;
+ const isReportPreviewAction = action?.actionName === CONST.REPORT.ACTIONS.TYPE.REPORT_PREVIEW;
+ const actorAccountID = ReportUtils.getReportActionActorAccountID(action, iouReport, report);
const [invoiceReceiverPolicy] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY}${report?.invoiceReceiver && 'policyID' in report.invoiceReceiver ? report.invoiceReceiver.policyID : -1}`);
let displayName = ReportUtils.getDisplayNameForParticipant(actorAccountID);
@@ -94,20 +96,13 @@ function ReportActionItemSingle({
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
let actorHint = (login || (displayName ?? '')).replace(CONST.REGEX.MERGED_ACCOUNT_PREFIX, '');
const isTripRoom = ReportUtils.isTripRoom(report);
- const isReportPreviewAction = action?.actionName === CONST.REPORT.ACTIONS.TYPE.REPORT_PREVIEW;
const displayAllActors = isReportPreviewAction && !isTripRoom && !ReportUtils.isPolicyExpenseChat(report);
const isInvoiceReport = ReportUtils.isInvoiceReport(iouReport ?? null);
const isWorkspaceActor = isInvoiceReport || (ReportUtils.isPolicyExpenseChat(report) && (!actorAccountID || displayAllActors));
- const ownerAccountID = iouReport?.ownerAccountID ?? action?.childOwnerAccountID;
+
let avatarSource = avatar;
let avatarId: number | string | undefined = actorAccountID;
- if (isReportPreviewAction && ReportUtils.isPolicyExpenseChat(report)) {
- avatarId = ownerAccountID;
- avatarSource = personalDetails[ownerAccountID ?? -1]?.avatar;
- displayName = ReportUtils.getDisplayNameForParticipant(ownerAccountID);
- actorHint = displayName;
- }
if (isWorkspaceActor) {
displayName = ReportUtils.getPolicyName(report, undefined, policy);
actorHint = displayName;
@@ -220,7 +215,7 @@ function ReportActionItemSingle({
}
return (
@@ -271,7 +266,7 @@ function ReportActionItemSingle({
) : null}
- {action?.delegateAccountID && !isReportPreviewAction && (
+ {!!action?.delegateAccountID && !isReportPreviewAction && (
{translate('delegate.onBehalfOfMessage', {delegator: accountOwnerDetails?.displayName ?? ''})}
)}
{children}
diff --git a/src/pages/home/report/ReportFooter.tsx b/src/pages/home/report/ReportFooter.tsx
index 7c4ec786b633..c087510374be 100644
--- a/src/pages/home/report/ReportFooter.tsx
+++ b/src/pages/home/report/ReportFooter.tsx
@@ -185,7 +185,7 @@ function ReportFooter({
return (
<>
- {shouldHideComposer && (
+ {!!shouldHideComposer && (
)}
{isArchivedRoom && }
- {!isArchivedRoom && isBlockedFromChat && }
+ {!isArchivedRoom && !!isBlockedFromChat && }
{!isAnonymousUser && !canWriteInReport && isSystemChat && }
{isAdminsOnlyPostingRoom && !isUserPolicyAdmin && !isArchivedRoom && !isAnonymousUser && !isBlockedFromChat && (
{convertToLTR(message ?? '')}
- {fragment?.isEdited && (
+ {!!fragment?.isEdited && (
<>
- {isLoading && optionListItems?.length === 0 && (
+ {!!isLoading && optionListItems?.length === 0 && (
diff --git a/src/pages/iou/request/step/IOURequestStepConfirmation.tsx b/src/pages/iou/request/step/IOURequestStepConfirmation.tsx
index 942b472734a2..8d5c5cac3eb5 100644
--- a/src/pages/iou/request/step/IOURequestStepConfirmation.tsx
+++ b/src/pages/iou/request/step/IOURequestStepConfirmation.tsx
@@ -624,7 +624,7 @@ function IOURequestStepConfirmation({
]}
/>
{isLoading && }
- {gpsRequired && (
+ {!!gpsRequired && (
setStartLocationPermissionFlow(false)}
diff --git a/src/pages/iou/request/step/IOURequestStepParticipants.tsx b/src/pages/iou/request/step/IOURequestStepParticipants.tsx
index c3fec439e042..34e77b19d078 100644
--- a/src/pages/iou/request/step/IOURequestStepParticipants.tsx
+++ b/src/pages/iou/request/step/IOURequestStepParticipants.tsx
@@ -183,7 +183,7 @@ function IOURequestStepParticipants({
testID={IOURequestStepParticipants.displayName}
includeSafeAreaPaddingBottom={false}
>
- {skipConfirmation && (
+ {!!skipConfirmation && (
{isLoadingReceipt && }
- {pdfFile && (
+ {!!pdfFile && (
{
openPicker({
- onPicked: setReceiptAndNavigate,
+ onPicked: (data) => setReceiptAndNavigate(data.at(0) ?? {}),
});
}}
>
@@ -680,7 +680,7 @@ function IOURequestStepScan({
)}
- {startLocationPermissionFlow && fileResize && (
+ {startLocationPermissionFlow && !!fileResize && (
setStartLocationPermissionFlow(false)}
diff --git a/src/pages/iou/request/step/IOURequestStepScan/index.tsx b/src/pages/iou/request/step/IOURequestStepScan/index.tsx
index 1f71f174dd23..4ed956e5ce7e 100644
--- a/src/pages/iou/request/step/IOURequestStepScan/index.tsx
+++ b/src/pages/iou/request/step/IOURequestStepScan/index.tsx
@@ -666,7 +666,7 @@ function IOURequestStepScan({
role={CONST.ROLE.BUTTON}
onPress={() => {
openPicker({
- onPicked: setReceiptAndNavigate,
+ onPicked: (data) => setReceiptAndNavigate(data.at(0) ?? {}),
});
}}
>
@@ -743,7 +743,7 @@ function IOURequestStepScan({
style={[styles.p9]}
onPress={() => {
openPicker({
- onPicked: setReceiptAndNavigate,
+ onPicked: (data) => setReceiptAndNavigate(data.at(0) ?? {}),
});
}}
/>
@@ -783,7 +783,7 @@ function IOURequestStepScan({
confirmText={translate('common.close')}
shouldShowCancelButton={false}
/>
- {startLocationPermissionFlow && fileResize && (
+ {startLocationPermissionFlow && !!fileResize && (
setStartLocationPermissionFlow(false)}
diff --git a/src/pages/iou/request/step/IOURequestStepTag.tsx b/src/pages/iou/request/step/IOURequestStepTag.tsx
index 90731e732d50..6c999d7a7f70 100644
--- a/src/pages/iou/request/step/IOURequestStepTag.tsx
+++ b/src/pages/iou/request/step/IOURequestStepTag.tsx
@@ -131,7 +131,7 @@ function IOURequestStepTag({
)}
)}
- {shouldShowTag && (
+ {!!shouldShowTag && (
<>
{translate('iou.tagSelection')} User.togglePlatformMute(platform, mutedPlatforms)}
/>
diff --git a/src/pages/settings/Profile/Contacts/ContactMethodDetailsPage.tsx b/src/pages/settings/Profile/Contacts/ContactMethodDetailsPage.tsx
index bd0151cda4ea..dc21701f65fe 100644
--- a/src/pages/settings/Profile/Contacts/ContactMethodDetailsPage.tsx
+++ b/src/pages/settings/Profile/Contacts/ContactMethodDetailsPage.tsx
@@ -135,14 +135,6 @@ function ContactMethodDetailsPage({route}: ContactMethodDetailsPageProps) {
User.deleteContactMethod(contactMethod, loginList ?? {}, backTo);
}, [contactMethod, loginList, toggleDeleteModal, backTo]);
- const sendValidateCode = () => {
- if (loginData?.validateCodeSent) {
- return;
- }
-
- User.requestContactMethodValidateCode(contactMethod);
- };
-
const prevValidatedDate = usePrevious(loginData?.validatedDate);
useEffect(() => {
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
@@ -276,7 +268,7 @@ function ContactMethodDetailsPage({route}: ContactMethodDetailsPageProps) {
Navigation.goBack(ROUTES.SETTINGS_CONTACT_METHODS.getRoute(backTo));
setIsValidateCodeActionModalVisible(false);
}}
- sendValidateCode={sendValidateCode}
+ sendValidateCode={() => User.requestContactMethodValidateCode(contactMethod)}
description={translate('contacts.enterMagicCode', {contactMethod})}
footer={() => getMenuItems()}
/>
diff --git a/src/pages/settings/Profile/Contacts/ContactMethodsPage.tsx b/src/pages/settings/Profile/Contacts/ContactMethodsPage.tsx
index 6c6d4268eccd..92a246949c53 100644
--- a/src/pages/settings/Profile/Contacts/ContactMethodsPage.tsx
+++ b/src/pages/settings/Profile/Contacts/ContactMethodsPage.tsx
@@ -41,9 +41,9 @@ function ContactMethodsPage({route}: ContactMethodsPageProps) {
// Sort the login names by placing the one corresponding to the default contact method as the first item before displaying the contact methods.
// The default contact method is determined by checking against the session email (the current login).
const sortedLoginNames = loginNames.sort((loginName) => (loginList?.[loginName].partnerUserID === session?.email ? -1 : 1));
-
const loginMenuItems = sortedLoginNames.map((loginName) => {
const login = loginList?.[loginName];
+ const isDefaultContactMethod = session?.email === login?.partnerUserID;
const pendingAction = login?.pendingFields?.deletedLogin ?? login?.pendingFields?.addedLogin ?? undefined;
if (!login?.partnerUserID && !pendingAction) {
return null;
@@ -60,7 +60,9 @@ function ContactMethodsPage({route}: ContactMethodsPageProps) {
let indicator;
if (Object.values(login?.errorFields ?? {}).some((errorField) => !isEmptyObject(errorField))) {
indicator = CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR;
- } else if (!login?.validatedDate) {
+ } else if (!login?.validatedDate && !isDefaultContactMethod) {
+ indicator = CONST.BRICK_ROAD_INDICATOR_STATUS.INFO;
+ } else if (!login?.validatedDate && isDefaultContactMethod && loginNames.length > 1) {
indicator = CONST.BRICK_ROAD_INDICATOR_STATUS.INFO;
}
diff --git a/src/pages/settings/Profile/Contacts/NewContactMethodPage.tsx b/src/pages/settings/Profile/Contacts/NewContactMethodPage.tsx
index 124d6525113b..c2a7e1b6712c 100644
--- a/src/pages/settings/Profile/Contacts/NewContactMethodPage.tsx
+++ b/src/pages/settings/Profile/Contacts/NewContactMethodPage.tsx
@@ -109,14 +109,6 @@ function NewContactMethodPage({route}: NewContactMethodPageProps) {
Navigation.goBack(ROUTES.SETTINGS_CONTACT_METHODS.getRoute(navigateBackTo));
}, [navigateBackTo]);
- const sendValidateCode = () => {
- if (loginData?.validateCodeSent) {
- return;
- }
-
- User.requestValidateCodeAction();
- };
-
return (
User.requestValidateCodeAction()}
description={translate('contacts.enterMagicCode', {contactMethod})}
/>
diff --git a/src/pages/settings/Security/AddDelegate/DelegateMagicCodeModal.tsx b/src/pages/settings/Security/AddDelegate/DelegateMagicCodeModal.tsx
index dd54aa5b9404..0393382415fc 100644
--- a/src/pages/settings/Security/AddDelegate/DelegateMagicCodeModal.tsx
+++ b/src/pages/settings/Security/AddDelegate/DelegateMagicCodeModal.tsx
@@ -46,14 +46,6 @@ function DelegateMagicCodeModal({login, role, onClose}: DelegateMagicCodeModalPr
Delegate.clearAddDelegateErrors(currentDelegate?.email ?? '', 'addDelegate');
};
- const sendValidateCode = () => {
- if (currentDelegate?.validateCodeSent) {
- return;
- }
-
- User.requestValidateCodeAction();
- };
-
return (
User.requestValidateCodeAction()}
+ hasMagicCodeBeenSent={!!currentDelegate?.validateCodeSent}
handleSubmitForm={(validateCode) => Delegate.addDelegate(login, role, validateCode)}
description={translate('delegate.enterMagicCode', {contactMethod: account?.primaryLogin ?? ''})}
/>
diff --git a/src/pages/settings/Security/SecuritySettingsPage.tsx b/src/pages/settings/Security/SecuritySettingsPage.tsx
index 42352594db7a..ec82000d06c8 100644
--- a/src/pages/settings/Security/SecuritySettingsPage.tsx
+++ b/src/pages/settings/Security/SecuritySettingsPage.tsx
@@ -236,7 +236,7 @@ function SecuritySettingsPage() {
shouldUseSingleExecution
/>
- {canUseNewDotCopilot && (
+ {!!canUseNewDotCopilot && (
TwoFactorAuthActions.quitAndNavigateBack(backTo)}
>
- {isUserValidated && (
+ {!!isUserValidated && (
}
- {account?.isEligibleForRefund && (
+ {!!account?.isEligibleForRefund && (
{translate('subscription.details.title')}
- {privateTaxExempt && (
+ {!!privateTaxExempt && (
{
let updatedDraftValues = draftValues;
if (!draftValues) {
@@ -146,17 +149,6 @@ function ExpensifyCardPage({
GetPhysicalCardUtils.goToNextPhysicalCardRoute(domain, GetPhysicalCardUtils.getUpdatedPrivatePersonalDetails(updatedDraftValues, privatePersonalDetails));
};
- const sendValidateCode = () => {
- const primaryLogin = account?.primaryLogin ?? '';
- const loginData = loginList?.[primaryLogin];
-
- if (loginData?.validateCodeSent) {
- return;
- }
-
- requestValidateCodeAction();
- };
-
if (isNotFound) {
return Navigation.goBack(ROUTES.SETTINGS_WALLET)} />;
}
@@ -209,7 +201,7 @@ function ExpensifyCardPage({
interactive={false}
titleStyle={styles.newKansasLarge}
/>
- {limitNameKey && limitTitleKey && (
+ {!!limitNameKey && !!limitTitleKey && (
{}}
- sendValidateCode={sendValidateCode}
+ sendValidateCode={() => requestValidateCodeAction()}
onClose={() => setIsValidateCodeActionModalVisible(false)}
isVisible={isValidateCodeActionModalVisible}
+ hasMagicCodeBeenSent={!!loginData?.validateCodeSent}
title={translate('cardPage.validateCardTitle')}
description={translate('cardPage.enterMagicCode', {contactMethod: account?.primaryLogin ?? ''})}
/>
diff --git a/src/pages/settings/Wallet/VerifyAccountPage.tsx b/src/pages/settings/Wallet/VerifyAccountPage.tsx
index a3b51ef0de17..200b6b55363a 100644
--- a/src/pages/settings/Wallet/VerifyAccountPage.tsx
+++ b/src/pages/settings/Wallet/VerifyAccountPage.tsx
@@ -69,6 +69,7 @@ function VerifyAccountPage({route}: VerifyAccountPageProps) {
sendValidateCode={() => User.requestValidateCodeAction()}
handleSubmitForm={handleSubmitForm}
validateError={validateLoginError}
+ hasMagicCodeBeenSent={!!loginData?.validateCodeSent}
isVisible={isValidateCodeActionModalVisible}
title={translate('contacts.validateAccount')}
description={translate('contacts.featureRequiresValidate')}
diff --git a/src/pages/signin/SignInPageLayout/index.tsx b/src/pages/signin/SignInPageLayout/index.tsx
index f63801dd158a..3517edf9b847 100644
--- a/src/pages/signin/SignInPageLayout/index.tsx
+++ b/src/pages/signin/SignInPageLayout/index.tsx
@@ -15,6 +15,10 @@ import useTheme from '@hooks/useTheme';
import useThemeStyles from '@hooks/useThemeStyles';
import useWindowDimensions from '@hooks/useWindowDimensions';
import * as Browser from '@libs/Browser';
+import DomUtils from '@libs/DomUtils';
+import getPlatform from '@libs/getPlatform';
+// eslint-disable-next-line no-restricted-imports
+import themes from '@styles/theme';
import variables from '@styles/variables';
import CONST from '@src/CONST';
import BackgroundImage from './BackgroundImage';
@@ -81,6 +85,27 @@ function SignInPageLayout(
const backgroundImageHeight = Math.max(variables.signInContentMinHeight, containerHeight);
+ /*
+ SignInPageLayout always has a dark theme regardless of the app theme. ThemeProvider sets auto-fill input styles globally so different ThemeProviders conflict and auto-fill input styles are incorrectly applied for this component.
+ Add a class to `body` when this component stays mounted and remove it when the component dismounts.
+ A new styleID is added with dark theme text with more specific css selector using this added cssClass.
+ */
+ const cssClass = 'sign-in-page-layout';
+ DomUtils.addCSS(DomUtils.getAutofilledInputStyle(themes[CONST.THEME.DARK].text, `.${cssClass}`), 'sign-in-autofill-input');
+
+ useEffect(() => {
+ const isWeb = getPlatform() === CONST.PLATFORM.WEB;
+ const isDesktop = getPlatform() === CONST.PLATFORM.DESKTOP;
+ if (!isWeb && !isDesktop) {
+ return;
+ }
+ // add css class to body only for web and desktop
+ document.body.classList.add(cssClass);
+ return () => {
+ document.body.classList.remove(cssClass);
+ };
+ }, []);
+
return (
{!shouldUseNarrowLayout ? (
diff --git a/src/pages/signin/SignUpWelcomeForm.tsx b/src/pages/signin/SignUpWelcomeForm.tsx
index be16475cdfc7..1f8687c218b7 100644
--- a/src/pages/signin/SignUpWelcomeForm.tsx
+++ b/src/pages/signin/SignUpWelcomeForm.tsx
@@ -32,7 +32,7 @@ function SignUpWelcomeForm() {
pressOnEnter
style={[styles.mb2]}
/>
- {serverErrorText && (
+ {!!serverErrorText && (
- {hasDestinationError && (
+ {!!hasDestinationError && (
= shouldUseNarrowLayout ? [styles.mhv12, styles.mhn5, styles.mbn5] : [styles.mhv8, styles.mhn8, styles.mbn5];
- const shouldShowAddress = !readOnly || formattedAddress;
+ const shouldShowAddress = !readOnly || !!formattedAddress;
const fetchPolicyData = useCallback(() => {
if (policyDraft?.id) {
@@ -248,7 +248,7 @@ function WorkspaceProfilePage({policyDraft, policy: policyProp, route}: Workspac
/>
- {canUseSpotnanaTravel && shouldShowAddress && (
+ {!!canUseSpotnanaTravel && shouldShowAddress && (
- {isJoinRequestPending && (
+ {!!isJoinRequestPending && (
{children}
{!shouldShowConfirmationModal && renderActiveIntegration()}
- {shouldShowConfirmationModal && activeIntegration?.integrationToDisconnect && (
+ {!!shouldShowConfirmationModal && !!activeIntegration?.integrationToDisconnect && (
{
if (!activeIntegration?.integrationToDisconnect) {
diff --git a/src/pages/workspace/accounting/PolicyAccountingPage.tsx b/src/pages/workspace/accounting/PolicyAccountingPage.tsx
index 3e1117d4a12a..878a2dcd6b22 100644
--- a/src/pages/workspace/accounting/PolicyAccountingPage.tsx
+++ b/src/pages/workspace/accounting/PolicyAccountingPage.tsx
@@ -73,7 +73,6 @@ function PolicyAccountingPage({policy}: PolicyAccountingPageProps) {
const [isDisconnectModalOpen, setIsDisconnectModalOpen] = useState(false);
const [datetimeToRelative, setDateTimeToRelative] = useState('');
const threeDotsMenuContainerRef = useRef(null);
- const {canUseNewDotQBD} = usePermissions();
const {startIntegrationFlow, popoverAnchorRefs} = useAccountingContext();
const route = useRoute();
@@ -84,8 +83,7 @@ function PolicyAccountingPage({policy}: PolicyAccountingPageProps) {
const isSyncInProgress = isConnectionInProgress(connectionSyncProgress, policy);
- const {QBD: qbdConnectionName, ...allConnectionNamesWithoutQBD} = CONST.POLICY.CONNECTIONS.NAME;
- const connectionNames = canUseNewDotQBD ? CONST.POLICY.CONNECTIONS.NAME : allConnectionNamesWithoutQBD;
+ const connectionNames = CONST.POLICY.CONNECTIONS.NAME;
const accountingIntegrations = Object.values(connectionNames);
const connectedIntegration = getConnectedIntegration(policy, accountingIntegrations) ?? connectionSyncProgress?.connectionName;
const synchronizationError = connectedIntegration && getSynchronizationErrorMessage(policy, connectedIntegration, isSyncInProgress, translate, styles);
@@ -532,7 +530,7 @@ function PolicyAccountingPage({policy}: PolicyAccountingPageProps) {
)}
- {otherIntegrationsItems && (
+ {!!otherIntegrationsItems && (
- customField && (
+ !!customField && (
- customField && (
+ !!customField && (
{
updateRecord({
diff --git a/src/pages/workspace/accounting/netsuite/import/NetSuiteImportCustomFieldNew/NetSuiteCustomSegmentMappingPicker.tsx b/src/pages/workspace/accounting/netsuite/import/NetSuiteImportCustomFieldNew/NetSuiteCustomSegmentMappingPicker.tsx
new file mode 100644
index 000000000000..87ada876d1a2
--- /dev/null
+++ b/src/pages/workspace/accounting/netsuite/import/NetSuiteImportCustomFieldNew/NetSuiteCustomSegmentMappingPicker.tsx
@@ -0,0 +1,65 @@
+import React from 'react';
+import {View} from 'react-native';
+import FormHelpMessage from '@components/FormHelpMessage';
+import SelectionList from '@components/SelectionList';
+import RadioListItem from '@components/SelectionList/RadioListItem';
+import useLocalize from '@hooks/useLocalize';
+import useThemeStyles from '@hooks/useThemeStyles';
+import CONST from '@src/CONST';
+
+type NetSuiteCustomListPickerProps = {
+ /** Selected mapping value */
+ value?: string;
+
+ /** Text to display on error message */
+ errorText?: string;
+
+ /** Callback to fire when mapping is selected */
+ onInputChange?: (value: string) => void;
+};
+
+function NetSuiteCustomSegmentMappingPicker({value, errorText, onInputChange}: NetSuiteCustomListPickerProps) {
+ const {translate} = useLocalize();
+ const styles = useThemeStyles();
+
+ const selectionData = [
+ {
+ text: translate(`workspace.netsuite.import.importCustomFields.customSegments.addForm.segmentTitle`),
+ keyForList: CONST.NETSUITE_CUSTOM_RECORD_TYPES.CUSTOM_SEGMENT,
+ isSelected: value === CONST.NETSUITE_CUSTOM_RECORD_TYPES.CUSTOM_SEGMENT,
+ value: CONST.NETSUITE_CUSTOM_RECORD_TYPES.CUSTOM_SEGMENT,
+ },
+ {
+ text: translate(`workspace.netsuite.import.importCustomFields.customSegments.addForm.recordTitle`),
+ keyForList: CONST.NETSUITE_CUSTOM_RECORD_TYPES.CUSTOM_RECORD,
+ isSelected: value === CONST.NETSUITE_CUSTOM_RECORD_TYPES.CUSTOM_RECORD,
+ value: CONST.NETSUITE_CUSTOM_RECORD_TYPES.CUSTOM_RECORD,
+ },
+ ];
+
+ return (
+ <>
+ {
+ onInputChange?.(selected.value);
+ }}
+ ListItem={RadioListItem}
+ initiallyFocusedOptionKey={value ?? CONST.INTEGRATION_ENTITY_MAP_TYPES.TAG}
+ shouldSingleExecuteRowSelect
+ shouldUpdateFocusedIndex
+ />
+ {!!errorText && (
+
+
+
+ )}
+ >
+ );
+}
+
+NetSuiteCustomSegmentMappingPicker.displayName = 'NetSuiteCustomSegmentMappingPicker';
+export default NetSuiteCustomSegmentMappingPicker;
diff --git a/src/pages/workspace/accounting/netsuite/import/NetSuiteImportCustomFieldNew/NetSuiteImportAddCustomListContent.tsx b/src/pages/workspace/accounting/netsuite/import/NetSuiteImportCustomFieldNew/NetSuiteImportAddCustomListContent.tsx
new file mode 100644
index 000000000000..fa9b9af238e0
--- /dev/null
+++ b/src/pages/workspace/accounting/netsuite/import/NetSuiteImportCustomFieldNew/NetSuiteImportAddCustomListContent.tsx
@@ -0,0 +1,148 @@
+import React, {useCallback, useMemo, useRef} from 'react';
+import type {ForwardedRef} from 'react';
+import {InteractionManager, View} from 'react-native';
+import type {OnyxEntry} from 'react-native-onyx';
+import ConnectionLayout from '@components/ConnectionLayout';
+import type {FormRef} from '@components/Form/types';
+import InteractiveStepSubHeader from '@components/InteractiveStepSubHeader';
+import type {InteractiveStepSubHeaderHandle} from '@components/InteractiveStepSubHeader';
+import useSubStep from '@hooks/useSubStep';
+import useThemeStyles from '@hooks/useThemeStyles';
+import * as Connections from '@libs/actions/connections/NetSuiteCommands';
+import * as FormActions from '@libs/actions/FormActions';
+import Navigation from '@libs/Navigation/Navigation';
+import type {CustomFieldSubStepWithPolicy} from '@pages/workspace/accounting/netsuite/types';
+import CONST from '@src/CONST';
+import ONYXKEYS from '@src/ONYXKEYS';
+import ROUTES from '@src/ROUTES';
+import INPUT_IDS from '@src/types/form/NetSuiteCustomFieldForm';
+import type {NetSuiteCustomFieldForm} from '@src/types/form/NetSuiteCustomFieldForm';
+import type {Policy} from '@src/types/onyx';
+import {getCustomListInitialSubstep, getSubstepValues} from './customUtils';
+import ChooseCustomListStep from './substeps/ChooseCustomListStep';
+import ConfirmCustomListStep from './substeps/ConfirmCustomListStep';
+import CustomListMappingStep from './substeps/CustomListMappingStep';
+import TransactionFieldIDStep from './substeps/TransactionFieldIDStep';
+
+type NetSuiteImportAddCustomListContentProps = {
+ policy: OnyxEntry;
+ draftValues: OnyxEntry;
+};
+
+const formSteps = [ChooseCustomListStep, TransactionFieldIDStep, CustomListMappingStep, ConfirmCustomListStep];
+
+function NetSuiteImportAddCustomListContent({policy, draftValues}: NetSuiteImportAddCustomListContentProps) {
+ const policyID = policy?.id ?? '-1';
+ const styles = useThemeStyles();
+ const ref: ForwardedRef = useRef(null);
+ const formRef = useRef(null);
+
+ const values = useMemo(() => getSubstepValues(draftValues), [draftValues]);
+ const startFrom = useMemo(() => getCustomListInitialSubstep(values), [values]);
+
+ const config = policy?.connections?.netsuite?.options?.config;
+ const customLists = useMemo(() => config?.syncOptions?.customLists ?? [], [config?.syncOptions]);
+
+ const handleFinishStep = useCallback(() => {
+ InteractionManager.runAfterInteractions(() => {
+ const updatedCustomLists = customLists.concat([
+ {
+ listName: values[INPUT_IDS.LIST_NAME],
+ internalID: values[INPUT_IDS.INTERNAL_ID],
+ transactionFieldID: values[INPUT_IDS.TRANSACTION_FIELD_ID],
+ mapping: values[INPUT_IDS.MAPPING] ?? CONST.INTEGRATION_ENTITY_MAP_TYPES.TAG,
+ },
+ ]);
+ Connections.updateNetSuiteCustomLists(
+ policyID,
+ updatedCustomLists,
+ customLists,
+ `${CONST.NETSUITE_CONFIG.IMPORT_CUSTOM_FIELDS.CUSTOM_LISTS}_${customLists.length}`,
+ CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD,
+ );
+ FormActions.clearDraftValues(ONYXKEYS.FORMS.NETSUITE_CUSTOM_LIST_ADD_FORM);
+ Navigation.goBack(ROUTES.POLICY_ACCOUNTING_NETSUITE_IMPORT_CUSTOM_FIELD_MAPPING.getRoute(policyID, CONST.NETSUITE_CONFIG.IMPORT_CUSTOM_FIELDS.CUSTOM_LISTS));
+ });
+ }, [values, customLists, policyID]);
+
+ const {
+ componentToRender: SubStep,
+ isEditing,
+ nextScreen,
+ prevScreen,
+ screenIndex,
+ moveTo,
+ goToTheLastStep,
+ } = useSubStep({
+ bodyContent: formSteps,
+ startFrom,
+ onFinished: handleFinishStep,
+ });
+
+ const handleBackButtonPress = () => {
+ if (isEditing) {
+ goToTheLastStep();
+ return;
+ }
+
+ // Clicking back on the first screen should go back to listing
+ if (screenIndex === CONST.NETSUITE_CUSTOM_FIELD_SUBSTEP_INDEXES.CUSTOM_LISTS.CUSTOM_LIST_PICKER) {
+ FormActions.clearDraftValues(ONYXKEYS.FORMS.NETSUITE_CUSTOM_LIST_ADD_FORM);
+ Navigation.goBack(ROUTES.POLICY_ACCOUNTING_NETSUITE_IMPORT_CUSTOM_FIELD_MAPPING.getRoute(policyID, CONST.NETSUITE_CONFIG.IMPORT_CUSTOM_FIELDS.CUSTOM_LISTS));
+ return;
+ }
+ ref.current?.movePrevious();
+ formRef.current?.resetErrors();
+ prevScreen();
+ };
+
+ const handleNextScreen = useCallback(() => {
+ if (isEditing) {
+ goToTheLastStep();
+ return;
+ }
+ ref.current?.moveNext();
+ nextScreen();
+ }, [goToTheLastStep, isEditing, nextScreen]);
+
+ return (
+
+
+
+
+
+
+
+
+ );
+}
+
+NetSuiteImportAddCustomListContent.displayName = 'NetSuiteImportAddCustomListContent';
+
+export default NetSuiteImportAddCustomListContent;
diff --git a/src/pages/workspace/accounting/netsuite/import/NetSuiteImportCustomFieldNew/NetSuiteImportAddCustomListPage.tsx b/src/pages/workspace/accounting/netsuite/import/NetSuiteImportCustomFieldNew/NetSuiteImportAddCustomListPage.tsx
index 84277e182d35..6121c6b7c495 100644
--- a/src/pages/workspace/accounting/netsuite/import/NetSuiteImportCustomFieldNew/NetSuiteImportAddCustomListPage.tsx
+++ b/src/pages/workspace/accounting/netsuite/import/NetSuiteImportCustomFieldNew/NetSuiteImportAddCustomListPage.tsx
@@ -1,188 +1,25 @@
-import React, {useCallback, useMemo, useRef} from 'react';
-import type {ForwardedRef} from 'react';
-import {InteractionManager, View} from 'react-native';
-import ConnectionLayout from '@components/ConnectionLayout';
-import FormProvider from '@components/Form/FormProvider';
-import type {FormInputErrors, FormOnyxValues, FormRef} from '@components/Form/types';
-import InteractiveStepSubHeader from '@components/InteractiveStepSubHeader';
-import type {InteractiveStepSubHeaderHandle} from '@components/InteractiveStepSubHeader';
-import useLocalize from '@hooks/useLocalize';
-import useSubStep from '@hooks/useSubStep';
-import useThemeStyles from '@hooks/useThemeStyles';
-import * as Connections from '@libs/actions/connections/NetSuiteCommands';
-import Navigation from '@libs/Navigation/Navigation';
-import * as ValidationUtils from '@libs/ValidationUtils';
-import type {CustomFieldSubStepWithPolicy} from '@pages/workspace/accounting/netsuite/types';
-import type {WithPolicyConnectionsProps} from '@pages/workspace/withPolicyConnections';
+import React from 'react';
+import {useOnyx} from 'react-native-onyx';
+import FullScreenLoadingIndicator from '@components/FullscreenLoadingIndicator';
import withPolicyConnections from '@pages/workspace/withPolicyConnections';
-import CONST from '@src/CONST';
+import type {WithPolicyConnectionsProps} from '@pages/workspace/withPolicyConnections';
import ONYXKEYS from '@src/ONYXKEYS';
-import ROUTES from '@src/ROUTES';
-import INPUT_IDS from '@src/types/form/NetSuiteCustomFieldForm';
-import ChooseCustomListStep from './substeps/ChooseCustomListStep';
-import ConfirmCustomListStep from './substeps/ConfirmCustomListStep';
-import MappingStep from './substeps/MappingStep';
-import TransactionFieldIDStep from './substeps/TransactionFieldIDStep';
-
-const formSteps = [ChooseCustomListStep, TransactionFieldIDStep, MappingStep, ConfirmCustomListStep];
+import isLoadingOnyxValue from '@src/types/utils/isLoadingOnyxValue';
+import NetSuiteImportAddCustomListContent from './NetSuiteImportAddCustomListContent';
function NetSuiteImportAddCustomListPage({policy}: WithPolicyConnectionsProps) {
- const policyID = policy?.id ?? '-1';
- const styles = useThemeStyles();
- const {translate} = useLocalize();
- const ref: ForwardedRef = useRef(null);
- const formRef = useRef(null);
-
- const config = policy?.connections?.netsuite?.options?.config;
- const customLists = useMemo(() => config?.syncOptions?.customLists ?? [], [config?.syncOptions]);
-
- const handleFinishStep = useCallback(() => {
- InteractionManager.runAfterInteractions(() => {
- Navigation.goBack(ROUTES.POLICY_ACCOUNTING_NETSUITE_IMPORT_CUSTOM_FIELD_MAPPING.getRoute(policyID, CONST.NETSUITE_CONFIG.IMPORT_CUSTOM_FIELDS.CUSTOM_LISTS));
- });
- }, [policyID]);
-
- const {
- componentToRender: SubStep,
- isEditing,
- nextScreen,
- prevScreen,
- screenIndex,
- moveTo,
- goToTheLastStep,
- } = useSubStep({
- bodyContent: formSteps,
- startFrom: CONST.NETSUITE_CUSTOM_FIELD_SUBSTEP_INDEXES.CUSTOM_LISTS.CUSTOM_LIST_PICKER,
- onFinished: handleFinishStep,
- });
-
- const handleBackButtonPress = () => {
- if (isEditing) {
- goToTheLastStep();
- return;
- }
-
- // Clicking back on the first screen should go back to listing
- if (screenIndex === CONST.NETSUITE_CUSTOM_FIELD_SUBSTEP_INDEXES.CUSTOM_LISTS.CUSTOM_LIST_PICKER) {
- Navigation.goBack(ROUTES.POLICY_ACCOUNTING_NETSUITE_IMPORT_CUSTOM_FIELD_MAPPING.getRoute(policyID, CONST.NETSUITE_CONFIG.IMPORT_CUSTOM_FIELDS.CUSTOM_LISTS));
- return;
- }
- ref.current?.movePrevious();
- formRef.current?.resetErrors();
- prevScreen();
- };
-
- const handleNextScreen = useCallback(() => {
- if (isEditing) {
- goToTheLastStep();
- return;
- }
- ref.current?.moveNext();
- nextScreen();
- }, [goToTheLastStep, isEditing, nextScreen]);
-
- const validate = useCallback(
- (values: FormOnyxValues): FormInputErrors => {
- const errors: FormInputErrors = {};
- switch (screenIndex) {
- case CONST.NETSUITE_CUSTOM_FIELD_SUBSTEP_INDEXES.CUSTOM_LISTS.CUSTOM_LIST_PICKER:
- return ValidationUtils.getFieldRequiredErrors(values, [INPUT_IDS.LIST_NAME]);
- case CONST.NETSUITE_CUSTOM_FIELD_SUBSTEP_INDEXES.CUSTOM_LISTS.TRANSACTION_FIELD_ID:
- if (!ValidationUtils.isRequiredFulfilled(values[INPUT_IDS.TRANSACTION_FIELD_ID])) {
- const fieldLabel = translate(`workspace.netsuite.import.importCustomFields.customLists.fields.transactionFieldID`);
- errors[INPUT_IDS.TRANSACTION_FIELD_ID] = translate('workspace.netsuite.import.importCustomFields.requiredFieldError', {fieldName: fieldLabel});
- } else if (customLists.find((customList) => customList.transactionFieldID.toLowerCase() === values[INPUT_IDS.TRANSACTION_FIELD_ID].toLowerCase())) {
- errors[INPUT_IDS.TRANSACTION_FIELD_ID] = translate('workspace.netsuite.import.importCustomFields.customLists.errors.uniqueTransactionFieldIDError');
- }
- return errors;
- case CONST.NETSUITE_CUSTOM_FIELD_SUBSTEP_INDEXES.CUSTOM_LISTS.MAPPING:
- if (!ValidationUtils.isRequiredFulfilled(values[INPUT_IDS.MAPPING])) {
- errors[INPUT_IDS.MAPPING] = translate('common.error.pleaseSelectOne');
- }
- return errors;
- default:
- return errors;
- }
- },
- [customLists, screenIndex, translate],
- );
-
- const updateNetSuiteCustomLists = useCallback(
- (formValues: FormOnyxValues) => {
- const updatedCustomLists = customLists.concat([
- {
- listName: formValues[INPUT_IDS.LIST_NAME],
- internalID: formValues[INPUT_IDS.INTERNAL_ID],
- transactionFieldID: formValues[INPUT_IDS.TRANSACTION_FIELD_ID],
- mapping: formValues[INPUT_IDS.MAPPING] ?? CONST.INTEGRATION_ENTITY_MAP_TYPES.TAG,
- },
- ]);
- Connections.updateNetSuiteCustomLists(
- policyID,
- updatedCustomLists,
- customLists,
- `${CONST.NETSUITE_CONFIG.IMPORT_CUSTOM_FIELDS.CUSTOM_LISTS}_${customLists.length}`,
- CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD,
- );
- nextScreen();
- },
- [customLists, nextScreen, policyID],
- );
+ const [draftValues, draftValuesMetadata] = useOnyx(ONYXKEYS.FORMS.NETSUITE_CUSTOM_LIST_ADD_FORM_DRAFT);
+ const isLoading = isLoadingOnyxValue(draftValuesMetadata);
- const selectionListForm = [CONST.NETSUITE_CUSTOM_FIELD_SUBSTEP_INDEXES.CUSTOM_LISTS.MAPPING as number].includes(screenIndex);
- const submitFlexAllowed = [
- CONST.NETSUITE_CUSTOM_FIELD_SUBSTEP_INDEXES.CUSTOM_LISTS.CUSTOM_LIST_PICKER as number,
- CONST.NETSUITE_CUSTOM_FIELD_SUBSTEP_INDEXES.CUSTOM_LISTS.TRANSACTION_FIELD_ID as number,
- ].includes(screenIndex);
+ if (isLoading) {
+ return ;
+ }
return (
-
-
-
-
-
-
-
-
-
-
+
);
}
diff --git a/src/pages/workspace/accounting/netsuite/import/NetSuiteImportCustomFieldNew/NetSuiteImportAddCustomSegmentContent.tsx b/src/pages/workspace/accounting/netsuite/import/NetSuiteImportCustomFieldNew/NetSuiteImportAddCustomSegmentContent.tsx
new file mode 100644
index 000000000000..31ecdf837696
--- /dev/null
+++ b/src/pages/workspace/accounting/netsuite/import/NetSuiteImportCustomFieldNew/NetSuiteImportAddCustomSegmentContent.tsx
@@ -0,0 +1,150 @@
+import React, {useCallback, useMemo, useRef, useState} from 'react';
+import type {ForwardedRef} from 'react';
+import {InteractionManager, View} from 'react-native';
+import type {OnyxEntry} from 'react-native-onyx';
+import type {ValueOf} from 'type-fest';
+import ConnectionLayout from '@components/ConnectionLayout';
+import type {FormRef} from '@components/Form/types';
+import InteractiveStepSubHeader from '@components/InteractiveStepSubHeader';
+import type {InteractiveStepSubHeaderHandle} from '@components/InteractiveStepSubHeader';
+import useSubStep from '@hooks/useSubStep';
+import useThemeStyles from '@hooks/useThemeStyles';
+import * as Connections from '@libs/actions/connections/NetSuiteCommands';
+import * as FormActions from '@libs/actions/FormActions';
+import Navigation from '@libs/Navigation/Navigation';
+import type {CustomFieldSubStepWithPolicy} from '@pages/workspace/accounting/netsuite/types';
+import CONST from '@src/CONST';
+import type {TranslationPaths} from '@src/languages/types';
+import ONYXKEYS from '@src/ONYXKEYS';
+import ROUTES from '@src/ROUTES';
+import INPUT_IDS from '@src/types/form/NetSuiteCustomFieldForm';
+import type {NetSuiteCustomFieldForm} from '@src/types/form/NetSuiteCustomFieldForm';
+import type {Policy} from '@src/types/onyx';
+import {getCustomSegmentInitialSubstep, getSubstepValues} from './customUtils';
+import ChooseSegmentTypeStep from './substeps/ChooseSegmentTypeStep';
+import ConfirmCustomSegmentStep from './substeps/ConfirmCustomSegmentList';
+import CustomSegmentInternalIdStep from './substeps/CustomSegmentInternalIdStep';
+import CustomSegmentMappingStep from './substeps/CustomSegmentMappingStep';
+import CustomSegmentNameStep from './substeps/CustomSegmentNameStep';
+import CustomSegmentScriptIdStep from './substeps/CustomSegmentScriptIdStep';
+
+type NetSuiteImportAddCustomSegmentContentProps = {
+ policy: OnyxEntry;
+ draftValues: OnyxEntry;
+};
+
+const formSteps = [ChooseSegmentTypeStep, CustomSegmentNameStep, CustomSegmentInternalIdStep, CustomSegmentScriptIdStep, CustomSegmentMappingStep, ConfirmCustomSegmentStep];
+
+function NetSuiteImportAddCustomSegmentContent({policy, draftValues}: NetSuiteImportAddCustomSegmentContentProps) {
+ const policyID = policy?.id ?? '-1';
+ const styles = useThemeStyles();
+ const ref: ForwardedRef = useRef(null);
+ const formRef = useRef(null);
+
+ const config = policy?.connections?.netsuite?.options?.config;
+ const customSegments = useMemo(() => config?.syncOptions?.customSegments ?? [], [config?.syncOptions]);
+ const [customSegmentType, setCustomSegmentType] = useState | undefined>();
+ const values = useMemo(() => getSubstepValues(draftValues), [draftValues]);
+ const startFrom = useMemo(() => getCustomSegmentInitialSubstep(values), [values]);
+ const handleFinishStep = useCallback(() => {
+ InteractionManager.runAfterInteractions(() => {
+ const updatedCustomSegments = customSegments.concat([
+ {
+ segmentName: values[INPUT_IDS.SEGMENT_NAME],
+ internalID: values[INPUT_IDS.INTERNAL_ID],
+ scriptID: values[INPUT_IDS.SCRIPT_ID],
+ mapping: values[INPUT_IDS.MAPPING] ?? CONST.INTEGRATION_ENTITY_MAP_TYPES.TAG,
+ },
+ ]);
+ Connections.updateNetSuiteCustomSegments(
+ policyID,
+ updatedCustomSegments,
+ customSegments,
+ `${CONST.NETSUITE_CONFIG.IMPORT_CUSTOM_FIELDS.CUSTOM_SEGMENTS}_${customSegments.length}`,
+ CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD,
+ );
+ FormActions.clearDraftValues(ONYXKEYS.FORMS.NETSUITE_CUSTOM_SEGMENT_ADD_FORM);
+
+ Navigation.goBack(ROUTES.POLICY_ACCOUNTING_NETSUITE_IMPORT_CUSTOM_FIELD_MAPPING.getRoute(policyID, CONST.NETSUITE_CONFIG.IMPORT_CUSTOM_FIELDS.CUSTOM_SEGMENTS));
+ });
+ }, [values, customSegments, policyID]);
+
+ const {
+ componentToRender: SubStep,
+ isEditing,
+ nextScreen,
+ prevScreen,
+ screenIndex,
+ moveTo,
+ goToTheLastStep,
+ } = useSubStep({bodyContent: formSteps, startFrom, onFinished: handleFinishStep});
+
+ const handleBackButtonPress = () => {
+ if (isEditing) {
+ goToTheLastStep();
+ return;
+ }
+
+ // Clicking back on the first screen should go back to listing
+ if (screenIndex === CONST.NETSUITE_CUSTOM_FIELD_SUBSTEP_INDEXES.CUSTOM_SEGMENTS.SEGMENT_TYPE) {
+ FormActions.clearDraftValues(ONYXKEYS.FORMS.NETSUITE_CUSTOM_SEGMENT_ADD_FORM);
+ Navigation.goBack(ROUTES.POLICY_ACCOUNTING_NETSUITE_IMPORT_CUSTOM_FIELD_MAPPING.getRoute(policyID, CONST.NETSUITE_CONFIG.IMPORT_CUSTOM_FIELDS.CUSTOM_SEGMENTS));
+ return;
+ }
+ ref.current?.movePrevious();
+ formRef.current?.resetErrors();
+ prevScreen();
+ };
+
+ const handleNextScreen = useCallback(() => {
+ if (isEditing) {
+ goToTheLastStep();
+ return;
+ }
+ ref.current?.moveNext();
+ nextScreen();
+ }, [goToTheLastStep, isEditing, nextScreen]);
+
+ return (
+
+
+
+
+
+
+
+
+ );
+}
+
+NetSuiteImportAddCustomSegmentContent.displayName = 'NetSuiteImportAddCustomSegmentContent';
+
+export default NetSuiteImportAddCustomSegmentContent;
diff --git a/src/pages/workspace/accounting/netsuite/import/NetSuiteImportCustomFieldNew/NetSuiteImportAddCustomSegmentPage.tsx b/src/pages/workspace/accounting/netsuite/import/NetSuiteImportCustomFieldNew/NetSuiteImportAddCustomSegmentPage.tsx
index 1e711a2b082e..ea08b4b15dfb 100644
--- a/src/pages/workspace/accounting/netsuite/import/NetSuiteImportCustomFieldNew/NetSuiteImportAddCustomSegmentPage.tsx
+++ b/src/pages/workspace/accounting/netsuite/import/NetSuiteImportCustomFieldNew/NetSuiteImportAddCustomSegmentPage.tsx
@@ -1,231 +1,25 @@
-import React, {useCallback, useMemo, useRef, useState} from 'react';
-import type {ForwardedRef} from 'react';
-import {InteractionManager, View} from 'react-native';
-import type {ValueOf} from 'type-fest';
-import ConnectionLayout from '@components/ConnectionLayout';
-import FormProvider from '@components/Form/FormProvider';
-import type {FormInputErrors, FormOnyxValues, FormRef} from '@components/Form/types';
-import InteractiveStepSubHeader from '@components/InteractiveStepSubHeader';
-import type {InteractiveStepSubHeaderHandle} from '@components/InteractiveStepSubHeader';
-import useLocalize from '@hooks/useLocalize';
-import useSubStep from '@hooks/useSubStep';
-import useThemeStyles from '@hooks/useThemeStyles';
-import * as Connections from '@libs/actions/connections/NetSuiteCommands';
-import Navigation from '@libs/Navigation/Navigation';
-import * as ValidationUtils from '@libs/ValidationUtils';
-import type {CustomFieldSubStepWithPolicy} from '@pages/workspace/accounting/netsuite/types';
-import type {WithPolicyConnectionsProps} from '@pages/workspace/withPolicyConnections';
+import React from 'react';
+import {useOnyx} from 'react-native-onyx';
+import FullScreenLoadingIndicator from '@components/FullscreenLoadingIndicator';
import withPolicyConnections from '@pages/workspace/withPolicyConnections';
-import CONST from '@src/CONST';
-import type {TranslationPaths} from '@src/languages/types';
+import type {WithPolicyConnectionsProps} from '@pages/workspace/withPolicyConnections';
import ONYXKEYS from '@src/ONYXKEYS';
-import ROUTES from '@src/ROUTES';
-import INPUT_IDS from '@src/types/form/NetSuiteCustomFieldForm';
-import ChooseSegmentTypeStep from './substeps/ChooseSegmentTypeStep';
-import ConfirmCustomSegmentList from './substeps/ConfirmCustomSegmentList';
-import CustomSegmentInternalIdStep from './substeps/CustomSegmentInternalIdStep';
-import CustomSegmentNameStep from './substeps/CustomSegmentNameStep';
-import CustomSegmentScriptIdStep from './substeps/CustomSegmentScriptIdStep';
-import MappingStep from './substeps/MappingStep';
-
-const formSteps = [ChooseSegmentTypeStep, CustomSegmentNameStep, CustomSegmentInternalIdStep, CustomSegmentScriptIdStep, MappingStep, ConfirmCustomSegmentList];
+import isLoadingOnyxValue from '@src/types/utils/isLoadingOnyxValue';
+import NetSuiteImportAddCustomSegmentContent from './NetSuiteImportAddCustomSegmentContent';
function NetSuiteImportAddCustomSegmentPage({policy}: WithPolicyConnectionsProps) {
- const policyID = policy?.id ?? '-1';
- const styles = useThemeStyles();
- const {translate} = useLocalize();
- const ref: ForwardedRef = useRef(null);
- const formRef = useRef(null);
-
- const config = policy?.connections?.netsuite?.options?.config;
- const customSegments = useMemo(() => config?.syncOptions?.customSegments ?? [], [config?.syncOptions]);
-
- const handleFinishStep = useCallback(() => {
- InteractionManager.runAfterInteractions(() => {
- Navigation.goBack(ROUTES.POLICY_ACCOUNTING_NETSUITE_IMPORT_CUSTOM_FIELD_MAPPING.getRoute(policyID, CONST.NETSUITE_CONFIG.IMPORT_CUSTOM_FIELDS.CUSTOM_SEGMENTS));
- });
- }, [policyID]);
-
- const {
- componentToRender: SubStep,
- isEditing,
- nextScreen,
- prevScreen,
- screenIndex,
- moveTo,
- goToTheLastStep,
- } = useSubStep({bodyContent: formSteps, startFrom: CONST.NETSUITE_CUSTOM_FIELD_SUBSTEP_INDEXES.CUSTOM_SEGMENTS.SEGMENT_TYPE, onFinished: handleFinishStep});
-
- const handleBackButtonPress = () => {
- if (isEditing) {
- goToTheLastStep();
- return;
- }
-
- // Clicking back on the first screen should go back to listing
- if (screenIndex === CONST.NETSUITE_CUSTOM_FIELD_SUBSTEP_INDEXES.CUSTOM_SEGMENTS.SEGMENT_TYPE) {
- Navigation.goBack(ROUTES.POLICY_ACCOUNTING_NETSUITE_IMPORT_CUSTOM_FIELD_MAPPING.getRoute(policyID, CONST.NETSUITE_CONFIG.IMPORT_CUSTOM_FIELDS.CUSTOM_SEGMENTS));
- return;
- }
- ref.current?.movePrevious();
- formRef.current?.resetErrors();
- prevScreen();
- };
-
- const handleNextScreen = useCallback(() => {
- if (isEditing) {
- goToTheLastStep();
- return;
- }
- ref.current?.moveNext();
- nextScreen();
- }, [goToTheLastStep, isEditing, nextScreen]);
-
- const [customSegmentType, setCustomSegmentType] = useState | undefined>();
-
- const validate = useCallback(
- (values: FormOnyxValues): FormInputErrors => {
- const errors: FormInputErrors = {};
- const customSegmentRecordType = customSegmentType ?? CONST.NETSUITE_CUSTOM_RECORD_TYPES.CUSTOM_SEGMENT;
- switch (screenIndex) {
- case CONST.NETSUITE_CUSTOM_FIELD_SUBSTEP_INDEXES.CUSTOM_SEGMENTS.SEGMENT_NAME:
- if (!ValidationUtils.isRequiredFulfilled(values[INPUT_IDS.SEGMENT_NAME])) {
- errors[INPUT_IDS.SEGMENT_NAME] = translate('workspace.netsuite.import.importCustomFields.requiredFieldError', {
- fieldName: translate(`workspace.netsuite.import.importCustomFields.customSegments.addForm.${customSegmentRecordType}Name`),
- });
- } else if (customSegments.find((customSegment) => customSegment.segmentName.toLowerCase() === values[INPUT_IDS.SEGMENT_NAME].toLowerCase())) {
- const fieldLabel = translate(`workspace.netsuite.import.importCustomFields.customSegments.fields.segmentName`);
- errors[INPUT_IDS.SEGMENT_NAME] = translate('workspace.netsuite.import.importCustomFields.customSegments.errors.uniqueFieldError', {fieldName: fieldLabel});
- }
- return errors;
- case CONST.NETSUITE_CUSTOM_FIELD_SUBSTEP_INDEXES.CUSTOM_SEGMENTS.INTERNAL_ID:
- if (!ValidationUtils.isRequiredFulfilled(values[INPUT_IDS.INTERNAL_ID])) {
- const fieldLabel = translate(`workspace.netsuite.import.importCustomFields.customSegments.fields.internalID`);
- errors[INPUT_IDS.INTERNAL_ID] = translate('workspace.netsuite.import.importCustomFields.requiredFieldError', {fieldName: fieldLabel});
- } else if (customSegments.find((customSegment) => customSegment.internalID.toLowerCase() === values[INPUT_IDS.INTERNAL_ID].toLowerCase())) {
- const fieldLabel = translate(`workspace.netsuite.import.importCustomFields.customSegments.fields.internalID`);
- errors[INPUT_IDS.INTERNAL_ID] = translate('workspace.netsuite.import.importCustomFields.customSegments.errors.uniqueFieldError', {fieldName: fieldLabel});
- }
- return errors;
- case CONST.NETSUITE_CUSTOM_FIELD_SUBSTEP_INDEXES.CUSTOM_SEGMENTS.SCRIPT_ID:
- if (!ValidationUtils.isRequiredFulfilled(values[INPUT_IDS.SCRIPT_ID])) {
- const fieldLabel = translate(
- `workspace.netsuite.import.importCustomFields.customSegments.fields.${
- customSegmentRecordType === CONST.NETSUITE_CUSTOM_RECORD_TYPES.CUSTOM_SEGMENT ? 'scriptID' : 'customRecordScriptID'
- }`,
- );
- errors[INPUT_IDS.SCRIPT_ID] = translate('workspace.netsuite.import.importCustomFields.requiredFieldError', {fieldName: fieldLabel});
- } else if (customSegments.find((customSegment) => customSegment.scriptID.toLowerCase() === values[INPUT_IDS.SCRIPT_ID].toLowerCase())) {
- const fieldLabel = translate(
- `workspace.netsuite.import.importCustomFields.customSegments.fields.${
- customSegmentRecordType === CONST.NETSUITE_CUSTOM_RECORD_TYPES.CUSTOM_SEGMENT ? 'scriptID' : 'customRecordScriptID'
- }`,
- );
- errors[INPUT_IDS.SCRIPT_ID] = translate('workspace.netsuite.import.importCustomFields.customSegments.errors.uniqueFieldError', {fieldName: fieldLabel});
- }
- return errors;
- case CONST.NETSUITE_CUSTOM_FIELD_SUBSTEP_INDEXES.CUSTOM_SEGMENTS.MAPPING:
- if (!ValidationUtils.isRequiredFulfilled(values[INPUT_IDS.MAPPING])) {
- errors[INPUT_IDS.MAPPING] = translate('common.error.pleaseSelectOne');
- }
- return errors;
- default:
- return errors;
- }
- },
- [customSegmentType, customSegments, screenIndex, translate],
- );
-
- const updateNetSuiteCustomSegments = useCallback(
- (formValues: FormOnyxValues) => {
- const updatedCustomSegments = customSegments.concat([
- {
- segmentName: formValues[INPUT_IDS.SEGMENT_NAME],
- internalID: formValues[INPUT_IDS.INTERNAL_ID],
- scriptID: formValues[INPUT_IDS.SCRIPT_ID],
- mapping: formValues[INPUT_IDS.MAPPING] ?? CONST.INTEGRATION_ENTITY_MAP_TYPES.TAG,
- },
- ]);
- Connections.updateNetSuiteCustomSegments(
- policyID,
- updatedCustomSegments,
- customSegments,
- `${CONST.NETSUITE_CONFIG.IMPORT_CUSTOM_FIELDS.CUSTOM_SEGMENTS}_${customSegments.length}`,
- CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD,
- );
- nextScreen();
- },
- [customSegments, nextScreen, policyID],
- );
-
- const renderSubStepContent = useMemo(
- () => (
-
- ),
- [SubStep, handleNextScreen, isEditing, moveTo, policy, policyID, screenIndex, customSegmentType],
- );
+ const [draftValues, draftValuesMetadata] = useOnyx(ONYXKEYS.FORMS.NETSUITE_CUSTOM_SEGMENT_ADD_FORM_DRAFT);
+ const isLoading = isLoadingOnyxValue(draftValuesMetadata);
- const selectionListForm = [CONST.NETSUITE_CUSTOM_FIELD_SUBSTEP_INDEXES.CUSTOM_SEGMENTS.MAPPING as number].includes(screenIndex);
- const submitFlexAllowed = [
- CONST.NETSUITE_CUSTOM_FIELD_SUBSTEP_INDEXES.CUSTOM_SEGMENTS.SEGMENT_NAME as number,
- CONST.NETSUITE_CUSTOM_FIELD_SUBSTEP_INDEXES.CUSTOM_SEGMENTS.INTERNAL_ID as number,
- CONST.NETSUITE_CUSTOM_FIELD_SUBSTEP_INDEXES.CUSTOM_SEGMENTS.SCRIPT_ID as number,
- ].includes(screenIndex);
+ if (isLoading) {
+ return ;
+ }
return (
-
-
-
-
-
- {screenIndex === CONST.NETSUITE_CUSTOM_FIELD_SUBSTEP_INDEXES.CUSTOM_SEGMENTS.SEGMENT_TYPE ? (
- renderSubStepContent
- ) : (
-
- {renderSubStepContent}
-
- )}
-
-
+
);
}
diff --git a/src/pages/workspace/accounting/netsuite/import/NetSuiteImportCustomFieldNew/customUtils.ts b/src/pages/workspace/accounting/netsuite/import/NetSuiteImportCustomFieldNew/customUtils.ts
new file mode 100644
index 000000000000..19fcad553740
--- /dev/null
+++ b/src/pages/workspace/accounting/netsuite/import/NetSuiteImportCustomFieldNew/customUtils.ts
@@ -0,0 +1,50 @@
+import type {OnyxEntry} from 'react-native-onyx';
+import CONST from '@src/CONST';
+import type {NetSuiteCustomFieldForm} from '@src/types/form';
+import INPUT_IDS from '@src/types/form/NetSuiteCustomFieldForm';
+
+function getCustomListInitialSubstep(values: NetSuiteCustomFieldForm) {
+ if (!values[INPUT_IDS.LIST_NAME]) {
+ return CONST.NETSUITE_CUSTOM_FIELD_SUBSTEP_INDEXES.CUSTOM_LISTS.CUSTOM_LIST_PICKER;
+ }
+ if (!values[INPUT_IDS.TRANSACTION_FIELD_ID]) {
+ return CONST.NETSUITE_CUSTOM_FIELD_SUBSTEP_INDEXES.CUSTOM_LISTS.TRANSACTION_FIELD_ID;
+ }
+ if (!values[INPUT_IDS.MAPPING]) {
+ return CONST.NETSUITE_CUSTOM_FIELD_SUBSTEP_INDEXES.CUSTOM_LISTS.MAPPING;
+ }
+ return CONST.NETSUITE_CUSTOM_FIELD_SUBSTEP_INDEXES.CUSTOM_LISTS.CONFIRM;
+}
+
+function getCustomSegmentInitialSubstep(values: NetSuiteCustomFieldForm) {
+ if (!values[INPUT_IDS.SEGMENT_TYPE]) {
+ return CONST.NETSUITE_CUSTOM_FIELD_SUBSTEP_INDEXES.CUSTOM_SEGMENTS.SEGMENT_TYPE;
+ }
+ if (!values[INPUT_IDS.SEGMENT_NAME]) {
+ return CONST.NETSUITE_CUSTOM_FIELD_SUBSTEP_INDEXES.CUSTOM_SEGMENTS.SEGMENT_NAME;
+ }
+ if (!values[INPUT_IDS.INTERNAL_ID]) {
+ return CONST.NETSUITE_CUSTOM_FIELD_SUBSTEP_INDEXES.CUSTOM_SEGMENTS.INTERNAL_ID;
+ }
+ if (!values[INPUT_IDS.SCRIPT_ID]) {
+ return CONST.NETSUITE_CUSTOM_FIELD_SUBSTEP_INDEXES.CUSTOM_SEGMENTS.SCRIPT_ID;
+ }
+ if (!values[INPUT_IDS.MAPPING]) {
+ return CONST.NETSUITE_CUSTOM_FIELD_SUBSTEP_INDEXES.CUSTOM_SEGMENTS.MAPPING;
+ }
+ return CONST.NETSUITE_CUSTOM_FIELD_SUBSTEP_INDEXES.CUSTOM_SEGMENTS.CONFIRM;
+}
+
+function getSubstepValues(NetSuitCustomFieldDraft: OnyxEntry): NetSuiteCustomFieldForm {
+ return {
+ [INPUT_IDS.LIST_NAME]: NetSuitCustomFieldDraft?.[INPUT_IDS.LIST_NAME] ?? '',
+ [INPUT_IDS.TRANSACTION_FIELD_ID]: NetSuitCustomFieldDraft?.[INPUT_IDS.TRANSACTION_FIELD_ID] ?? '',
+ [INPUT_IDS.MAPPING]: NetSuitCustomFieldDraft?.[INPUT_IDS.MAPPING] ?? '',
+ [INPUT_IDS.INTERNAL_ID]: NetSuitCustomFieldDraft?.[INPUT_IDS.INTERNAL_ID] ?? '',
+ [INPUT_IDS.SCRIPT_ID]: NetSuitCustomFieldDraft?.[INPUT_IDS.SCRIPT_ID] ?? '',
+ [INPUT_IDS.SEGMENT_TYPE]: NetSuitCustomFieldDraft?.[INPUT_IDS.SEGMENT_TYPE] ?? '',
+ [INPUT_IDS.SEGMENT_NAME]: NetSuitCustomFieldDraft?.[INPUT_IDS.SEGMENT_NAME] ?? '',
+ };
+}
+
+export {getSubstepValues, getCustomListInitialSubstep, getCustomSegmentInitialSubstep};
diff --git a/src/pages/workspace/accounting/netsuite/import/NetSuiteImportCustomFieldNew/substeps/ChooseCustomListStep.tsx b/src/pages/workspace/accounting/netsuite/import/NetSuiteImportCustomFieldNew/substeps/ChooseCustomListStep.tsx
index 473a01d5e7ce..b51ad476a10b 100644
--- a/src/pages/workspace/accounting/netsuite/import/NetSuiteImportCustomFieldNew/substeps/ChooseCustomListStep.tsx
+++ b/src/pages/workspace/accounting/netsuite/import/NetSuiteImportCustomFieldNew/substeps/ChooseCustomListStep.tsx
@@ -1,26 +1,54 @@
-import React from 'react';
+import React, {useCallback} from 'react';
+import FormProvider from '@components/Form/FormProvider';
import InputWrapper from '@components/Form/InputWrapper';
+import type {FormInputErrors, FormOnyxValues} from '@components/Form/types';
import Text from '@components/Text';
import useLocalize from '@hooks/useLocalize';
+import useNetSuiteImportAddCustomListFormSubmit from '@hooks/useNetSuiteImportAddCustomListForm';
import useThemeStyles from '@hooks/useThemeStyles';
+import * as ValidationUtils from '@libs/ValidationUtils';
import NetSuiteCustomListPicker from '@pages/workspace/accounting/netsuite/import/NetSuiteImportCustomFieldNew/NetSuiteCustomListPicker';
import type {CustomFieldSubStepWithPolicy} from '@pages/workspace/accounting/netsuite/types';
+import ONYXKEYS from '@src/ONYXKEYS';
import INPUT_IDS from '@src/types/form/NetSuiteCustomFieldForm';
-function ChooseCustomListStep({policy}: CustomFieldSubStepWithPolicy) {
+const STEP_FIELDS = [INPUT_IDS.LIST_NAME, INPUT_IDS.INTERNAL_ID];
+
+function ChooseCustomListStep({policy, onNext, isEditing, netSuiteCustomFieldFormValues}: CustomFieldSubStepWithPolicy) {
const styles = useThemeStyles();
const {translate} = useLocalize();
+ const validate = useCallback((values: FormOnyxValues): FormInputErrors => {
+ return ValidationUtils.getFieldRequiredErrors(values, [INPUT_IDS.LIST_NAME]);
+ }, []);
+
+ const handleSubmit = useNetSuiteImportAddCustomListFormSubmit({
+ fieldIds: STEP_FIELDS,
+ onNext,
+ shouldSaveDraft: true,
+ });
+
return (
- <>
+ {translate(`workspace.netsuite.import.importCustomFields.customLists.addForm.listNameTitle`)}
- >
+
);
}
diff --git a/src/pages/workspace/accounting/netsuite/import/NetSuiteImportCustomFieldNew/substeps/ChooseSegmentTypeStep.tsx b/src/pages/workspace/accounting/netsuite/import/NetSuiteImportCustomFieldNew/substeps/ChooseSegmentTypeStep.tsx
index a5bbb15a1756..5077064cf6a0 100644
--- a/src/pages/workspace/accounting/netsuite/import/NetSuiteImportCustomFieldNew/substeps/ChooseSegmentTypeStep.tsx
+++ b/src/pages/workspace/accounting/netsuite/import/NetSuiteImportCustomFieldNew/substeps/ChooseSegmentTypeStep.tsx
@@ -1,74 +1,71 @@
-import React, {useState} from 'react';
-import {View} from 'react-native';
-import FormHelpMessage from '@components/FormHelpMessage';
-import SelectionList from '@components/SelectionList';
-import RadioListItem from '@components/SelectionList/RadioListItem';
+import React, {useCallback} from 'react';
+import type {ValueOf} from 'type-fest';
+import FormProvider from '@components/Form/FormProvider';
+import InputWrapper from '@components/Form/InputWrapper';
+import type {FormInputErrors, FormOnyxValues} from '@components/Form/types';
import Text from '@components/Text';
import useLocalize from '@hooks/useLocalize';
+import useNetSuiteImportAddCustomSegmentFormSubmit from '@hooks/useNetSuiteImportAddCustomSegmentForm';
import useThemeStyles from '@hooks/useThemeStyles';
+import * as ValidationUtils from '@libs/ValidationUtils';
+import NetSuiteCustomSegmentMappingPicker from '@pages/workspace/accounting/netsuite/import/NetSuiteImportCustomFieldNew/NetSuiteCustomSegmentMappingPicker';
import type {CustomFieldSubStepWithPolicy} from '@pages/workspace/accounting/netsuite/types';
-import CONST from '@src/CONST';
+import type CONST from '@src/CONST';
+import ONYXKEYS from '@src/ONYXKEYS';
+import INPUT_IDS from '@src/types/form/NetSuiteCustomFieldForm';
-function ChooseSegmentTypeStep({onNext, customSegmentType, setCustomSegmentType}: CustomFieldSubStepWithPolicy) {
+const STEP_FIELDS = [INPUT_IDS.SEGMENT_TYPE];
+
+function ChooseSegmentTypeStep({onNext, setCustomSegmentType, isEditing, netSuiteCustomFieldFormValues}: CustomFieldSubStepWithPolicy) {
const styles = useThemeStyles();
const {translate} = useLocalize();
- const [selectedType, setSelectedType] = useState(customSegmentType);
- const [isError, setIsError] = useState(false);
- const selectionData = [
- {
- text: translate(`workspace.netsuite.import.importCustomFields.customSegments.addForm.segmentTitle`),
- keyForList: CONST.NETSUITE_CUSTOM_RECORD_TYPES.CUSTOM_SEGMENT,
- isSelected: selectedType === CONST.NETSUITE_CUSTOM_RECORD_TYPES.CUSTOM_SEGMENT,
- value: CONST.NETSUITE_CUSTOM_RECORD_TYPES.CUSTOM_SEGMENT,
- },
- {
- text: translate(`workspace.netsuite.import.importCustomFields.customSegments.addForm.recordTitle`),
- keyForList: CONST.NETSUITE_CUSTOM_RECORD_TYPES.CUSTOM_RECORD,
- isSelected: selectedType === CONST.NETSUITE_CUSTOM_RECORD_TYPES.CUSTOM_RECORD,
- value: CONST.NETSUITE_CUSTOM_RECORD_TYPES.CUSTOM_RECORD,
+ const validate = useCallback(
+ (values: FormOnyxValues): FormInputErrors => {
+ const errors: FormInputErrors = {};
+
+ if (!ValidationUtils.isRequiredFulfilled(values[INPUT_IDS.SEGMENT_TYPE])) {
+ errors[INPUT_IDS.SEGMENT_TYPE] = translate('common.error.pleaseSelectOne');
+ }
+
+ return errors;
},
- ];
+ [translate],
+ );
- const onConfirm = () => {
- if (!selectedType) {
- setIsError(true);
- } else {
- setCustomSegmentType?.(selectedType);
+ const handleSubmit = useNetSuiteImportAddCustomSegmentFormSubmit({
+ fieldIds: STEP_FIELDS,
+ onNext: () => {
+ setCustomSegmentType?.(netSuiteCustomFieldFormValues[INPUT_IDS.SEGMENT_TYPE] as ValueOf);
onNext();
- }
- };
+ },
+ shouldSaveDraft: true,
+ });
return (
- <>
+
{translate(`workspace.netsuite.import.importCustomFields.customSegments.addForm.segmentRecordType`)}
{translate(`workspace.netsuite.import.importCustomFields.chooseOptionBelow`)}
- {
- setSelectedType(selected.value);
- setIsError(false);
- }}
- shouldSingleExecuteRowSelect
- shouldUpdateFocusedIndex
- showConfirmButton
- confirmButtonText={translate('common.next')}
- onConfirm={onConfirm}
- >
- {isError && (
-
-
-
- )}
-
- >
+
+
);
}
diff --git a/src/pages/workspace/accounting/netsuite/import/NetSuiteImportCustomFieldNew/substeps/ConfirmCustomListStep.tsx b/src/pages/workspace/accounting/netsuite/import/NetSuiteImportCustomFieldNew/substeps/ConfirmCustomListStep.tsx
index e72aa8710753..44525875c271 100644
--- a/src/pages/workspace/accounting/netsuite/import/NetSuiteImportCustomFieldNew/substeps/ConfirmCustomListStep.tsx
+++ b/src/pages/workspace/accounting/netsuite/import/NetSuiteImportCustomFieldNew/substeps/ConfirmCustomListStep.tsx
@@ -1,36 +1,54 @@
import React from 'react';
import {View} from 'react-native';
-import InputWrapper from '@components/Form/InputWrapper';
+import Button from '@components/Button';
+import FullScreenLoadingIndicator from '@components/FullscreenLoadingIndicator';
+import MenuItemWithTopDescription from '@components/MenuItemWithTopDescription';
import Text from '@components/Text';
import useLocalize from '@hooks/useLocalize';
+import useNetwork from '@hooks/useNetwork';
import useThemeStyles from '@hooks/useThemeStyles';
-import NetSuiteMenuWithTopDescriptionForm from '@pages/workspace/accounting/netsuite/import/NetSuiteImportCustomFieldNew/NetSuiteMenuWithTopDescriptionForm';
import type {CustomFieldSubStepWithPolicy} from '@pages/workspace/accounting/netsuite/types';
import type {TranslationPaths} from '@src/languages/types';
import INPUT_IDS from '@src/types/form/NetSuiteCustomFieldForm';
-function ConfirmCustomListStep({onMove}: CustomFieldSubStepWithPolicy) {
+function ConfirmCustomListStep({onMove, netSuiteCustomFieldFormValues: values, onNext}: CustomFieldSubStepWithPolicy) {
const styles = useThemeStyles();
const {translate} = useLocalize();
+ const {isOffline} = useNetwork();
const fieldNames = [INPUT_IDS.LIST_NAME, INPUT_IDS.TRANSACTION_FIELD_ID, INPUT_IDS.MAPPING];
+ if (!values.mapping) {
+ return ;
+ }
+
return (
-
+ {translate('workspace.common.letsDoubleCheck')}
{fieldNames.map((fieldName, index) => (
- {
onMove(index);
}}
- valueRenderer={(value) => (fieldName === INPUT_IDS.MAPPING && value ? translate(`workspace.netsuite.import.importTypes.${value}.label` as TranslationPaths) : value)}
/>
))}
+
+
+
);
}
diff --git a/src/pages/workspace/accounting/netsuite/import/NetSuiteImportCustomFieldNew/substeps/ConfirmCustomSegmentList.tsx b/src/pages/workspace/accounting/netsuite/import/NetSuiteImportCustomFieldNew/substeps/ConfirmCustomSegmentList.tsx
index bf1314491c03..73963a95d87b 100644
--- a/src/pages/workspace/accounting/netsuite/import/NetSuiteImportCustomFieldNew/substeps/ConfirmCustomSegmentList.tsx
+++ b/src/pages/workspace/accounting/netsuite/import/NetSuiteImportCustomFieldNew/substeps/ConfirmCustomSegmentList.tsx
@@ -1,29 +1,33 @@
import React from 'react';
import {View} from 'react-native';
-import InputWrapper from '@components/Form/InputWrapper';
+import Button from '@components/Button';
+import FullScreenLoadingIndicator from '@components/FullscreenLoadingIndicator';
+import MenuItemWithTopDescription from '@components/MenuItemWithTopDescription';
import Text from '@components/Text';
import useLocalize from '@hooks/useLocalize';
+import useNetwork from '@hooks/useNetwork';
import useThemeStyles from '@hooks/useThemeStyles';
-import NetSuiteMenuWithTopDescriptionForm from '@pages/workspace/accounting/netsuite/import/NetSuiteImportCustomFieldNew/NetSuiteMenuWithTopDescriptionForm';
import type {CustomFieldSubStepWithPolicy} from '@pages/workspace/accounting/netsuite/types';
import CONST from '@src/CONST';
import type {TranslationPaths} from '@src/languages/types';
import INPUT_IDS from '@src/types/form/NetSuiteCustomFieldForm';
-function ConfirmCustomListStep({onMove, customSegmentType}: CustomFieldSubStepWithPolicy) {
+function ConfirmCustomSegmentStep({onMove, customSegmentType, netSuiteCustomFieldFormValues: values, onNext}: CustomFieldSubStepWithPolicy) {
const styles = useThemeStyles();
const {translate} = useLocalize();
const fieldNames = [INPUT_IDS.SEGMENT_NAME, INPUT_IDS.INTERNAL_ID, INPUT_IDS.SCRIPT_ID, INPUT_IDS.MAPPING];
+ const {isOffline} = useNetwork();
+
+ if (!values.mapping) {
+ return ;
+ }
return (
{translate('workspace.common.letsDoubleCheck')}
{fieldNames.map((fieldName, index) => (
- {
onMove(index + 1);
}}
- valueRenderer={(value) => (fieldName === INPUT_IDS.MAPPING && value ? translate(`workspace.netsuite.import.importTypes.${value}.label` as TranslationPaths) : value)}
/>
))}
+
+
+
);
}
-ConfirmCustomListStep.displayName = 'ConfirmCustomListStep';
-export default ConfirmCustomListStep;
+ConfirmCustomSegmentStep.displayName = 'ConfirmCustomSegmentStep';
+export default ConfirmCustomSegmentStep;
diff --git a/src/pages/workspace/accounting/netsuite/import/NetSuiteImportCustomFieldNew/substeps/CustomListMappingStep.tsx b/src/pages/workspace/accounting/netsuite/import/NetSuiteImportCustomFieldNew/substeps/CustomListMappingStep.tsx
new file mode 100644
index 000000000000..6602b83bb1a7
--- /dev/null
+++ b/src/pages/workspace/accounting/netsuite/import/NetSuiteImportCustomFieldNew/substeps/CustomListMappingStep.tsx
@@ -0,0 +1,74 @@
+import React, {useCallback} from 'react';
+import FormProvider from '@components/Form/FormProvider';
+import InputWrapper from '@components/Form/InputWrapper';
+import type {FormInputErrors, FormOnyxValues} from '@components/Form/types';
+import Text from '@components/Text';
+import useLocalize from '@hooks/useLocalize';
+import useNetSuiteImportAddCustomListFormSubmit from '@hooks/useNetSuiteImportAddCustomListForm';
+import useThemeStyles from '@hooks/useThemeStyles';
+import * as ValidationUtils from '@libs/ValidationUtils';
+import NetSuiteCustomFieldMappingPicker from '@pages/workspace/accounting/netsuite/import/NetSuiteImportCustomFieldNew/NetSuiteCustomFieldMappingPicker';
+import type {CustomFieldSubStepWithPolicy} from '@pages/workspace/accounting/netsuite/types';
+import CONST from '@src/CONST';
+import type {TranslationPaths} from '@src/languages/types';
+import ONYXKEYS from '@src/ONYXKEYS';
+import INPUT_IDS from '@src/types/form/NetSuiteCustomFieldForm';
+
+const STEP_FIELDS = [INPUT_IDS.MAPPING];
+
+function CustomListMappingStep({importCustomField, customSegmentType, onNext, isEditing, netSuiteCustomFieldFormValues}: CustomFieldSubStepWithPolicy) {
+ const styles = useThemeStyles();
+ const {translate} = useLocalize();
+
+ const validate = useCallback(
+ (values: FormOnyxValues): FormInputErrors => {
+ const errors: FormInputErrors = {};
+
+ if (!ValidationUtils.isRequiredFulfilled(values[INPUT_IDS.MAPPING])) {
+ errors[INPUT_IDS.MAPPING] = translate('common.error.pleaseSelectOne');
+ }
+
+ return errors;
+ },
+ [translate],
+ );
+
+ const handleSubmit = useNetSuiteImportAddCustomListFormSubmit({
+ fieldIds: STEP_FIELDS,
+ onNext,
+ shouldSaveDraft: true,
+ });
+
+ let titleKey;
+ if (importCustomField === CONST.NETSUITE_CONFIG.IMPORT_CUSTOM_FIELDS.CUSTOM_LISTS) {
+ titleKey = 'workspace.netsuite.import.importCustomFields.customLists.addForm.mappingTitle';
+ } else {
+ const customSegmentRecordType = customSegmentType ?? CONST.NETSUITE_CUSTOM_RECORD_TYPES.CUSTOM_SEGMENT;
+ titleKey = `workspace.netsuite.import.importCustomFields.customSegments.addForm.${customSegmentRecordType}MappingTitle`;
+ }
+
+ return (
+
+ {translate(titleKey as TranslationPaths)}
+ {translate(`workspace.netsuite.import.importCustomFields.chooseOptionBelow`)}
+
+
+ );
+}
+
+CustomListMappingStep.displayName = 'CustomListMappingStep';
+export default CustomListMappingStep;
diff --git a/src/pages/workspace/accounting/netsuite/import/NetSuiteImportCustomFieldNew/substeps/CustomSegmentInternalIdStep.tsx b/src/pages/workspace/accounting/netsuite/import/NetSuiteImportCustomFieldNew/substeps/CustomSegmentInternalIdStep.tsx
index df110dfb1eeb..5342a452baf9 100644
--- a/src/pages/workspace/accounting/netsuite/import/NetSuiteImportCustomFieldNew/substeps/CustomSegmentInternalIdStep.tsx
+++ b/src/pages/workspace/accounting/netsuite/import/NetSuiteImportCustomFieldNew/substeps/CustomSegmentInternalIdStep.tsx
@@ -1,18 +1,25 @@
-import React from 'react';
+import React, {useCallback} from 'react';
import {View} from 'react-native';
+import FormProvider from '@components/Form/FormProvider';
import InputWrapper from '@components/Form/InputWrapper';
+import type {FormInputErrors, FormOnyxValues} from '@components/Form/types';
import RenderHTML from '@components/RenderHTML';
import Text from '@components/Text';
import TextInput from '@components/TextInput';
import useAutoFocusInput from '@hooks/useAutoFocusInput';
import useLocalize from '@hooks/useLocalize';
+import useNetSuiteImportAddCustomSegmentFormSubmit from '@hooks/useNetSuiteImportAddCustomSegmentForm';
import useThemeStyles from '@hooks/useThemeStyles';
import Parser from '@libs/Parser';
+import * as ValidationUtils from '@libs/ValidationUtils';
import type {CustomFieldSubStepWithPolicy} from '@pages/workspace/accounting/netsuite/types';
import CONST from '@src/CONST';
+import ONYXKEYS from '@src/ONYXKEYS';
import INPUT_IDS from '@src/types/form/NetSuiteCustomFieldForm';
-function CustomSegmentInternalIdStep({customSegmentType}: CustomFieldSubStepWithPolicy) {
+const STEP_FIELDS = [INPUT_IDS.INTERNAL_ID];
+
+function CustomSegmentInternalIdStep({customSegmentType, onNext, isEditing, netSuiteCustomFieldFormValues, customSegments}: CustomFieldSubStepWithPolicy) {
const styles = useThemeStyles();
const {translate} = useLocalize();
const {inputCallbackRef} = useAutoFocusInput();
@@ -21,26 +28,62 @@ function CustomSegmentInternalIdStep({customSegmentType}: CustomFieldSubStepWith
const fieldLabel = translate(`workspace.netsuite.import.importCustomFields.customSegments.fields.internalID`);
+ const handleSubmit = useNetSuiteImportAddCustomSegmentFormSubmit({
+ fieldIds: STEP_FIELDS,
+ onNext,
+ shouldSaveDraft: true,
+ });
+
+ const validate = useCallback(
+ (values: FormOnyxValues): FormInputErrors => {
+ const errors: FormInputErrors = {};
+
+ if (!ValidationUtils.isRequiredFulfilled(values[INPUT_IDS.INTERNAL_ID])) {
+ errors[INPUT_IDS.INTERNAL_ID] = translate('workspace.netsuite.import.importCustomFields.requiredFieldError', {fieldName: fieldLabel});
+ } else if (customSegments?.find((customSegment) => customSegment.internalID.toLowerCase() === values[INPUT_IDS.INTERNAL_ID].toLowerCase())) {
+ errors[INPUT_IDS.INTERNAL_ID] = translate('workspace.netsuite.import.importCustomFields.customSegments.errors.uniqueFieldError', {fieldName: fieldLabel});
+ }
+ return errors;
+ },
+ [customSegments, translate, fieldLabel],
+ );
+
return (
-
-
- {translate(`workspace.netsuite.import.importCustomFields.customSegments.addForm.customSegmentInternalIDTitle`)}
-
-
-
- ${Parser.replace(translate(`workspace.netsuite.import.importCustomFields.customSegments.addForm.${customSegmentRecordType}InternalIDFooter`))}`}
+
+
+
+ {translate(`workspace.netsuite.import.importCustomFields.customSegments.addForm.customSegmentInternalIDTitle`)}
+
+
+
+
+ ${Parser.replace(
+ translate(`workspace.netsuite.import.importCustomFields.customSegments.addForm.${customSegmentRecordType}InternalIDFooter`),
+ )}`}
+ />
+
-
+
);
}
diff --git a/src/pages/workspace/accounting/netsuite/import/NetSuiteImportCustomFieldNew/substeps/CustomSegmentMappingStep.tsx b/src/pages/workspace/accounting/netsuite/import/NetSuiteImportCustomFieldNew/substeps/CustomSegmentMappingStep.tsx
new file mode 100644
index 000000000000..6686d86850c3
--- /dev/null
+++ b/src/pages/workspace/accounting/netsuite/import/NetSuiteImportCustomFieldNew/substeps/CustomSegmentMappingStep.tsx
@@ -0,0 +1,74 @@
+import React, {useCallback} from 'react';
+import FormProvider from '@components/Form/FormProvider';
+import InputWrapper from '@components/Form/InputWrapper';
+import type {FormInputErrors, FormOnyxValues} from '@components/Form/types';
+import Text from '@components/Text';
+import useLocalize from '@hooks/useLocalize';
+import useNetSuiteImportAddCustomSegmentFormSubmit from '@hooks/useNetSuiteImportAddCustomSegmentForm';
+import useThemeStyles from '@hooks/useThemeStyles';
+import * as ValidationUtils from '@libs/ValidationUtils';
+import NetSuiteCustomFieldMappingPicker from '@pages/workspace/accounting/netsuite/import/NetSuiteImportCustomFieldNew/NetSuiteCustomFieldMappingPicker';
+import type {CustomFieldSubStepWithPolicy} from '@pages/workspace/accounting/netsuite/types';
+import CONST from '@src/CONST';
+import type {TranslationPaths} from '@src/languages/types';
+import ONYXKEYS from '@src/ONYXKEYS';
+import INPUT_IDS from '@src/types/form/NetSuiteCustomFieldForm';
+
+const STEP_FIELDS = [INPUT_IDS.MAPPING];
+
+function CustomSegmentMappingStep({importCustomField, customSegmentType, onNext, isEditing, netSuiteCustomFieldFormValues}: CustomFieldSubStepWithPolicy) {
+ const styles = useThemeStyles();
+ const {translate} = useLocalize();
+ // reminder to change the validation logic at the last phase of PR
+
+ const validate = useCallback(
+ (values: FormOnyxValues): FormInputErrors => {
+ const errors: FormInputErrors = {};
+ if (!ValidationUtils.isRequiredFulfilled(values[INPUT_IDS.MAPPING])) {
+ errors[INPUT_IDS.MAPPING] = translate('common.error.pleaseSelectOne');
+ }
+ return errors;
+ },
+ [translate],
+ );
+
+ const handleSubmit = useNetSuiteImportAddCustomSegmentFormSubmit({
+ fieldIds: STEP_FIELDS,
+ onNext,
+ shouldSaveDraft: true,
+ });
+
+ let titleKey;
+ if (importCustomField === CONST.NETSUITE_CONFIG.IMPORT_CUSTOM_FIELDS.CUSTOM_LISTS) {
+ titleKey = 'workspace.netsuite.import.importCustomFields.customLists.addForm.mappingTitle';
+ } else {
+ const customSegmentRecordType = customSegmentType ?? CONST.NETSUITE_CUSTOM_RECORD_TYPES.CUSTOM_SEGMENT;
+ titleKey = `workspace.netsuite.import.importCustomFields.customSegments.addForm.${customSegmentRecordType}MappingTitle`;
+ }
+
+ return (
+
+ {translate(titleKey as TranslationPaths)}
+ {translate(`workspace.netsuite.import.importCustomFields.chooseOptionBelow`)}
+
+
+ );
+}
+
+CustomSegmentMappingStep.displayName = 'CustomSegmentMappingStep';
+export default CustomSegmentMappingStep;
diff --git a/src/pages/workspace/accounting/netsuite/import/NetSuiteImportCustomFieldNew/substeps/CustomSegmentNameStep.tsx b/src/pages/workspace/accounting/netsuite/import/NetSuiteImportCustomFieldNew/substeps/CustomSegmentNameStep.tsx
index 3d2af112743f..ad3d4d8c39ca 100644
--- a/src/pages/workspace/accounting/netsuite/import/NetSuiteImportCustomFieldNew/substeps/CustomSegmentNameStep.tsx
+++ b/src/pages/workspace/accounting/netsuite/import/NetSuiteImportCustomFieldNew/substeps/CustomSegmentNameStep.tsx
@@ -1,18 +1,24 @@
-import React from 'react';
+import React, {useCallback} from 'react';
import {View} from 'react-native';
+import FormProvider from '@components/Form/FormProvider';
import InputWrapper from '@components/Form/InputWrapper';
+import type {FormInputErrors, FormOnyxValues} from '@components/Form/types';
import RenderHTML from '@components/RenderHTML';
import Text from '@components/Text';
import TextInput from '@components/TextInput';
import useAutoFocusInput from '@hooks/useAutoFocusInput';
import useLocalize from '@hooks/useLocalize';
+import useNetSuiteImportAddCustomSegmentFormSubmit from '@hooks/useNetSuiteImportAddCustomSegmentForm';
import useThemeStyles from '@hooks/useThemeStyles';
import Parser from '@libs/Parser';
+import * as ValidationUtils from '@libs/ValidationUtils';
import type {CustomFieldSubStepWithPolicy} from '@pages/workspace/accounting/netsuite/types';
import CONST from '@src/CONST';
+import ONYXKEYS from '@src/ONYXKEYS';
import INPUT_IDS from '@src/types/form/NetSuiteCustomFieldForm';
-function CustomSegmentNameStep({customSegmentType}: CustomFieldSubStepWithPolicy) {
+const STEP_FIELDS = [INPUT_IDS.SEGMENT_NAME];
+function CustomSegmentNameStep({customSegmentType, onNext, isEditing, customSegments}: CustomFieldSubStepWithPolicy) {
const styles = useThemeStyles();
const {translate} = useLocalize();
const {inputCallbackRef} = useAutoFocusInput();
@@ -21,26 +27,60 @@ function CustomSegmentNameStep({customSegmentType}: CustomFieldSubStepWithPolicy
const customSegmentRecordType = customSegmentType ?? CONST.NETSUITE_CUSTOM_RECORD_TYPES.CUSTOM_SEGMENT;
+ const handleSubmit = useNetSuiteImportAddCustomSegmentFormSubmit({
+ fieldIds: STEP_FIELDS,
+ onNext,
+ shouldSaveDraft: true,
+ });
+
+ const validate = useCallback(
+ (values: FormOnyxValues): FormInputErrors => {
+ const errors: FormInputErrors = {};
+
+ if (!ValidationUtils.isRequiredFulfilled(values[INPUT_IDS.SEGMENT_NAME])) {
+ errors[INPUT_IDS.SEGMENT_NAME] = translate('workspace.netsuite.import.importCustomFields.requiredFieldError', {
+ fieldName: translate(`workspace.netsuite.import.importCustomFields.customSegments.addForm.${customSegmentRecordType}Name`),
+ });
+ } else if (customSegments?.find((customSegment) => customSegment.segmentName.toLowerCase() === values[INPUT_IDS.SEGMENT_NAME].toLowerCase())) {
+ errors[INPUT_IDS.SEGMENT_NAME] = translate('workspace.netsuite.import.importCustomFields.customSegments.errors.uniqueFieldError', {fieldName: fieldLabel});
+ }
+ return errors;
+ },
+ [customSegments, translate, customSegmentRecordType, fieldLabel],
+ );
+
return (
-
-
- {translate(`workspace.netsuite.import.importCustomFields.customSegments.addForm.${customSegmentRecordType}NameTitle`)}
-
-
-
- ${Parser.replace(translate(`workspace.netsuite.import.importCustomFields.customSegments.addForm.${customSegmentRecordType}NameFooter`))}`}
+
+
+
+ {translate(`workspace.netsuite.import.importCustomFields.customSegments.addForm.${customSegmentRecordType}NameTitle`)}
+
+
+
+ ${Parser.replace(translate(`workspace.netsuite.import.importCustomFields.customSegments.addForm.${customSegmentRecordType}NameFooter`))}`}
+ />
+
-
+
);
}
diff --git a/src/pages/workspace/accounting/netsuite/import/NetSuiteImportCustomFieldNew/substeps/CustomSegmentScriptIdStep.tsx b/src/pages/workspace/accounting/netsuite/import/NetSuiteImportCustomFieldNew/substeps/CustomSegmentScriptIdStep.tsx
index 1388f6b2397e..529c720dfb9f 100644
--- a/src/pages/workspace/accounting/netsuite/import/NetSuiteImportCustomFieldNew/substeps/CustomSegmentScriptIdStep.tsx
+++ b/src/pages/workspace/accounting/netsuite/import/NetSuiteImportCustomFieldNew/substeps/CustomSegmentScriptIdStep.tsx
@@ -1,18 +1,25 @@
-import React from 'react';
+import React, {useCallback} from 'react';
import {View} from 'react-native';
+import FormProvider from '@components/Form/FormProvider';
import InputWrapper from '@components/Form/InputWrapper';
+import type {FormInputErrors, FormOnyxValues} from '@components/Form/types';
import RenderHTML from '@components/RenderHTML';
import Text from '@components/Text';
import TextInput from '@components/TextInput';
import useAutoFocusInput from '@hooks/useAutoFocusInput';
import useLocalize from '@hooks/useLocalize';
+import useNetSuiteImportAddCustomSegmentFormSubmit from '@hooks/useNetSuiteImportAddCustomSegmentForm';
import useThemeStyles from '@hooks/useThemeStyles';
import Parser from '@libs/Parser';
+import * as ValidationUtils from '@libs/ValidationUtils';
import type {CustomFieldSubStepWithPolicy} from '@pages/workspace/accounting/netsuite/types';
import CONST from '@src/CONST';
+import type {TranslationPaths} from '@src/languages/types';
+import ONYXKEYS from '@src/ONYXKEYS';
import INPUT_IDS from '@src/types/form/NetSuiteCustomFieldForm';
-function CustomSegmentScriptIdStep({customSegmentType}: CustomFieldSubStepWithPolicy) {
+const STEP_FIELDS = [INPUT_IDS.SCRIPT_ID];
+function CustomSegmentScriptIdStep({customSegmentType, onNext, isEditing, customSegments}: CustomFieldSubStepWithPolicy) {
const styles = useThemeStyles();
const {translate} = useLocalize();
const {inputCallbackRef} = useAutoFocusInput();
@@ -25,26 +32,60 @@ function CustomSegmentScriptIdStep({customSegmentType}: CustomFieldSubStepWithPo
}`,
);
+ const validate = useCallback(
+ (values: FormOnyxValues): FormInputErrors => {
+ const errors: FormInputErrors = {};
+
+ if (!ValidationUtils.isRequiredFulfilled(values[INPUT_IDS.SCRIPT_ID])) {
+ errors[INPUT_IDS.SCRIPT_ID] = translate('workspace.netsuite.import.importCustomFields.requiredFieldError', {fieldName: fieldLabel});
+ } else if (customSegments?.find((customSegment) => customSegment.scriptID.toLowerCase() === values[INPUT_IDS.SCRIPT_ID].toLowerCase())) {
+ errors[INPUT_IDS.SCRIPT_ID] = translate('workspace.netsuite.import.importCustomFields.customSegments.errors.uniqueFieldError', {fieldName: fieldLabel});
+ }
+ return errors;
+ },
+ [customSegments, translate, fieldLabel],
+ );
+
+ const handleSubmit = useNetSuiteImportAddCustomSegmentFormSubmit({
+ fieldIds: STEP_FIELDS,
+ onNext,
+ shouldSaveDraft: true,
+ });
+
return (
-
-
- {translate(`workspace.netsuite.import.importCustomFields.customSegments.addForm.${customSegmentRecordType}ScriptIDTitle`)}
-
-
-
- ${Parser.replace(translate(`workspace.netsuite.import.importCustomFields.customSegments.addForm.${customSegmentRecordType}ScriptIDFooter`))}`}
+
+
+
+ {translate(`workspace.netsuite.import.importCustomFields.customSegments.addForm.${customSegmentRecordType}ScriptIDTitle` as TranslationPaths)}
+
+
+
+ ${Parser.replace(
+ translate(`workspace.netsuite.import.importCustomFields.customSegments.addForm.${customSegmentRecordType}ScriptIDFooter` as TranslationPaths),
+ )}`}
+ />
+
-
+
);
}
diff --git a/src/pages/workspace/accounting/netsuite/import/NetSuiteImportCustomFieldNew/substeps/MappingStep.tsx b/src/pages/workspace/accounting/netsuite/import/NetSuiteImportCustomFieldNew/substeps/MappingStep.tsx
deleted file mode 100644
index f41269e4954f..000000000000
--- a/src/pages/workspace/accounting/netsuite/import/NetSuiteImportCustomFieldNew/substeps/MappingStep.tsx
+++ /dev/null
@@ -1,37 +0,0 @@
-import React from 'react';
-import InputWrapper from '@components/Form/InputWrapper';
-import Text from '@components/Text';
-import useLocalize from '@hooks/useLocalize';
-import useThemeStyles from '@hooks/useThemeStyles';
-import NetSuiteCustomFieldMappingPicker from '@pages/workspace/accounting/netsuite/import/NetSuiteImportCustomFieldNew/NetSuiteCustomFieldMappingPicker';
-import type {CustomFieldSubStepWithPolicy} from '@pages/workspace/accounting/netsuite/types';
-import CONST from '@src/CONST';
-import type {TranslationPaths} from '@src/languages/types';
-import INPUT_IDS from '@src/types/form/NetSuiteCustomFieldForm';
-
-function MappingStep({importCustomField, customSegmentType}: CustomFieldSubStepWithPolicy) {
- const styles = useThemeStyles();
- const {translate} = useLocalize();
-
- let titleKey;
- if (importCustomField === CONST.NETSUITE_CONFIG.IMPORT_CUSTOM_FIELDS.CUSTOM_LISTS) {
- titleKey = 'workspace.netsuite.import.importCustomFields.customLists.addForm.mappingTitle';
- } else {
- const customSegmentRecordType = customSegmentType ?? CONST.NETSUITE_CUSTOM_RECORD_TYPES.CUSTOM_SEGMENT;
- titleKey = `workspace.netsuite.import.importCustomFields.customSegments.addForm.${customSegmentRecordType}MappingTitle`;
- }
-
- return (
- <>
- {translate(titleKey as TranslationPaths)}
- {translate(`workspace.netsuite.import.importCustomFields.chooseOptionBelow`)}
-
- >
- );
-}
-
-MappingStep.displayName = 'MappingStep';
-export default MappingStep;
diff --git a/src/pages/workspace/accounting/netsuite/import/NetSuiteImportCustomFieldNew/substeps/TransactionFieldIDStep.tsx b/src/pages/workspace/accounting/netsuite/import/NetSuiteImportCustomFieldNew/substeps/TransactionFieldIDStep.tsx
index 08c0ce4127d2..1b787212f0ac 100644
--- a/src/pages/workspace/accounting/netsuite/import/NetSuiteImportCustomFieldNew/substeps/TransactionFieldIDStep.tsx
+++ b/src/pages/workspace/accounting/netsuite/import/NetSuiteImportCustomFieldNew/substeps/TransactionFieldIDStep.tsx
@@ -1,39 +1,79 @@
-import React from 'react';
+import React, {useCallback} from 'react';
import {View} from 'react-native';
+import FormProvider from '@components/Form/FormProvider';
import InputWrapper from '@components/Form/InputWrapper';
+import type {FormInputErrors, FormOnyxValues} from '@components/Form/types';
import RenderHTML from '@components/RenderHTML';
import Text from '@components/Text';
import TextInput from '@components/TextInput';
import useAutoFocusInput from '@hooks/useAutoFocusInput';
import useLocalize from '@hooks/useLocalize';
+import useNetSuiteImportAddCustomListFormSubmit from '@hooks/useNetSuiteImportAddCustomListForm';
import useThemeStyles from '@hooks/useThemeStyles';
import Parser from '@libs/Parser';
+import * as ValidationUtils from '@libs/ValidationUtils';
+import type {CustomFieldSubStepWithPolicy} from '@pages/workspace/accounting/netsuite/types';
import CONST from '@src/CONST';
+import ONYXKEYS from '@src/ONYXKEYS';
import INPUT_IDS from '@src/types/form/NetSuiteCustomFieldForm';
-function TransactionFieldIDStep() {
+const STEP_FIELDS = [INPUT_IDS.TRANSACTION_FIELD_ID];
+
+function TransactionFieldIDStep({onNext, isEditing, netSuiteCustomFieldFormValues, customLists}: CustomFieldSubStepWithPolicy) {
const styles = useThemeStyles();
const {translate} = useLocalize();
const {inputCallbackRef} = useAutoFocusInput();
const fieldLabel = translate(`workspace.netsuite.import.importCustomFields.customLists.fields.transactionFieldID`);
+ const handleSubmit = useNetSuiteImportAddCustomListFormSubmit({
+ fieldIds: STEP_FIELDS,
+ onNext,
+ shouldSaveDraft: true,
+ });
+
+ const validate = useCallback(
+ (values: FormOnyxValues): FormInputErrors => {
+ const errors: FormInputErrors = {};
+ if (!ValidationUtils.isRequiredFulfilled(values[INPUT_IDS.TRANSACTION_FIELD_ID])) {
+ errors[INPUT_IDS.TRANSACTION_FIELD_ID] = translate('workspace.netsuite.import.importCustomFields.requiredFieldError', {fieldName: fieldLabel});
+ } else if (customLists?.find((customList) => customList.transactionFieldID.toLowerCase() === values[INPUT_IDS.TRANSACTION_FIELD_ID].toLowerCase())) {
+ errors[INPUT_IDS.TRANSACTION_FIELD_ID] = translate('workspace.netsuite.import.importCustomFields.customLists.errors.uniqueTransactionFieldIDError');
+ }
+ return errors;
+ },
+ [customLists, translate, fieldLabel],
+ );
+
return (
-
- {translate(`workspace.netsuite.import.importCustomFields.customLists.addForm.transactionFieldIDTitle`)}
-
-
- ${Parser.replace(translate(`workspace.netsuite.import.importCustomFields.customLists.addForm.transactionFieldIDFooter`))}`} />
+
+
+ {translate(`workspace.netsuite.import.importCustomFields.customLists.addForm.transactionFieldIDTitle`)}
+
+
+ ${Parser.replace(translate(`workspace.netsuite.import.importCustomFields.customLists.addForm.transactionFieldIDFooter`))}`} />
+
-
+
);
}
diff --git a/src/pages/workspace/accounting/netsuite/import/NetSuiteImportCustomFieldView.tsx b/src/pages/workspace/accounting/netsuite/import/NetSuiteImportCustomFieldView.tsx
index d4c831d6155e..cb6513e785c1 100644
--- a/src/pages/workspace/accounting/netsuite/import/NetSuiteImportCustomFieldView.tsx
+++ b/src/pages/workspace/accounting/netsuite/import/NetSuiteImportCustomFieldView.tsx
@@ -96,7 +96,7 @@ function NetSuiteImportCustomFieldView({
shouldBeBlocked={!customField}
onBackButtonPress={() => Navigation.goBack(ROUTES.POLICY_ACCOUNTING_NETSUITE_IMPORT_CUSTOM_FIELD_MAPPING.getRoute(policyID, importCustomField))}
>
- {customField && (
+ {!!customField && (
) => void;
+
+ /** NetSuiteCustFieldForm values */
+ netSuiteCustomFieldFormValues: NetSuiteCustomFieldForm;
+
+ customSegments?: NetSuiteCustomSegment[];
+
+ customLists?: NetSuiteCustomList[];
};
type CustomListSelectorType = SelectorType & {
diff --git a/src/pages/workspace/accounting/qbd/advanced/QuickbooksDesktopAdvancedPage.tsx b/src/pages/workspace/accounting/qbd/advanced/QuickbooksDesktopAdvancedPage.tsx
index cd54127d3a44..298f26105505 100644
--- a/src/pages/workspace/accounting/qbd/advanced/QuickbooksDesktopAdvancedPage.tsx
+++ b/src/pages/workspace/accounting/qbd/advanced/QuickbooksDesktopAdvancedPage.tsx
@@ -1,7 +1,6 @@
import React from 'react';
import ConnectionLayout from '@components/ConnectionLayout';
import useLocalize from '@hooks/useLocalize';
-import usePermissions from '@hooks/usePermissions';
import useThemeStyles from '@hooks/useThemeStyles';
import * as QuickbooksDesktop from '@libs/actions/connections/QuickbooksDesktop';
import * as ErrorUtils from '@libs/ErrorUtils';
@@ -19,7 +18,6 @@ function QuickbooksDesktopAdvancedPage({policy}: WithPolicyConnectionsProps) {
const {translate} = useLocalize();
const policyID = policy?.id ?? '-1';
const qbdConfig = policy?.connections?.quickbooksDesktop?.config;
- const {canUseNewDotQBD} = usePermissions();
const qbdToggleSettingItems = [
{
@@ -50,9 +48,8 @@ function QuickbooksDesktopAdvancedPage({policy}: WithPolicyConnectionsProps) {
vendor.id === qbdConfig?.export?.nonReimbursableBillDefaultVendor);
- const {canUseNewDotQBD} = usePermissions();
const nonReimbursable = qbdConfig?.export?.nonReimbursable;
const nonReimbursableAccount = qbdConfig?.export?.nonReimbursableAccount;
@@ -58,8 +56,7 @@ function QuickbooksDesktopCompanyCardExpenseAccountPage({policy}: WithPolicyConn
displayName={QuickbooksDesktopCompanyCardExpenseAccountPage.displayName}
headerTitle="workspace.accounting.exportCompanyCard"
title="workspace.qbd.exportCompanyCardsDescription"
- accessVariants={[CONST.POLICY.ACCESS_VARIANTS.ADMIN]}
- shouldBeBlocked={!canUseNewDotQBD} // TODO: [QBD] remove it once the QBD beta is done
+ accessVariants={[CONST.POLICY.ACCESS_VARIANTS.ADMIN, CONST.POLICY.ACCESS_VARIANTS.CONTROL]}
featureName={CONST.POLICY.MORE_FEATURES.ARE_CONNECTIONS_ENABLED}
contentContainerStyle={styles.pb2}
titleStyle={styles.ph5}
diff --git a/src/pages/workspace/accounting/qbd/export/QuickbooksDesktopCompanyCardExpenseAccountSelectCardPage.tsx b/src/pages/workspace/accounting/qbd/export/QuickbooksDesktopCompanyCardExpenseAccountSelectCardPage.tsx
index 285d50e77022..8b7ea59bac80 100644
--- a/src/pages/workspace/accounting/qbd/export/QuickbooksDesktopCompanyCardExpenseAccountSelectCardPage.tsx
+++ b/src/pages/workspace/accounting/qbd/export/QuickbooksDesktopCompanyCardExpenseAccountSelectCardPage.tsx
@@ -4,7 +4,6 @@ import type {ListItem} from '@components/SelectionList/types';
import SelectionScreen from '@components/SelectionScreen';
import type {SelectorType} from '@components/SelectionScreen';
import useLocalize from '@hooks/useLocalize';
-import usePermissions from '@hooks/usePermissions';
import useThemeStyles from '@hooks/useThemeStyles';
import * as QuickbooksDesktop from '@libs/actions/connections/QuickbooksDesktop';
import * as ErrorUtils from '@libs/ErrorUtils';
@@ -29,7 +28,6 @@ function QuickbooksDesktopCompanyCardExpenseAccountSelectCardPage({policy}: With
const policyID = policy?.id ?? '-1';
const qbdConfig = policy?.connections?.quickbooksDesktop?.config;
const {creditCardAccounts, payableAccounts, vendors, bankAccounts} = policy?.connections?.quickbooksDesktop?.data ?? {};
- const {canUseNewDotQBD} = usePermissions();
const nonReimbursable = qbdConfig?.export?.nonReimbursable;
const nonReimbursableAccount = qbdConfig?.export?.nonReimbursableAccount;
const nonReimbursableBillDefaultVendor = qbdConfig?.export?.nonReimbursableBillDefaultVendor;
@@ -88,12 +86,11 @@ function QuickbooksDesktopCompanyCardExpenseAccountSelectCardPage({policy}: With
return (
selectExportCompanyCard(selection as MenuItem)}
shouldSingleExecuteRowSelect
diff --git a/src/pages/workspace/accounting/qbd/export/QuickbooksDesktopCompanyCardExpenseAccountSelectPage.tsx b/src/pages/workspace/accounting/qbd/export/QuickbooksDesktopCompanyCardExpenseAccountSelectPage.tsx
index c8da940f5209..d7cbf62c6d8b 100644
--- a/src/pages/workspace/accounting/qbd/export/QuickbooksDesktopCompanyCardExpenseAccountSelectPage.tsx
+++ b/src/pages/workspace/accounting/qbd/export/QuickbooksDesktopCompanyCardExpenseAccountSelectPage.tsx
@@ -6,7 +6,6 @@ import type {ListItem} from '@components/SelectionList/types';
import SelectionScreen from '@components/SelectionScreen';
import Text from '@components/Text';
import useLocalize from '@hooks/useLocalize';
-import usePermissions from '@hooks/usePermissions';
import useThemeStyles from '@hooks/useThemeStyles';
import * as QuickbooksDesktop from '@libs/actions/connections/QuickbooksDesktop';
import * as ConnectionUtils from '@libs/ConnectionUtils';
@@ -31,7 +30,6 @@ function QuickbooksDesktopCompanyCardExpenseAccountSelectPage({policy}: WithPoli
const styles = useThemeStyles();
const policyID = policy?.id ?? '-1';
const qbdConfig = policy?.connections?.quickbooksDesktop?.config;
- const {canUseNewDotQBD} = usePermissions();
const nonReimbursable = qbdConfig?.export?.nonReimbursable;
const nonReimbursableAccount = qbdConfig?.export?.nonReimbursableAccount;
@@ -71,12 +69,11 @@ function QuickbooksDesktopCompanyCardExpenseAccountSelectPage({policy}: WithPoli
return (
{translate(`workspace.qbd.accounts.${nonReimbursable}AccountDescription`)} : null}
- shouldBeBlocked={!canUseNewDotQBD} // TODO: [QBD] remove it once the QBD beta is done
sections={data.length ? [{data}] : []}
listItem={RadioListItem}
onSelectRow={selectExportAccount}
diff --git a/src/pages/workspace/accounting/qbd/export/QuickbooksDesktopExportDateSelectPage.tsx b/src/pages/workspace/accounting/qbd/export/QuickbooksDesktopExportDateSelectPage.tsx
index c4f648283338..ac4c8bbb4e39 100644
--- a/src/pages/workspace/accounting/qbd/export/QuickbooksDesktopExportDateSelectPage.tsx
+++ b/src/pages/workspace/accounting/qbd/export/QuickbooksDesktopExportDateSelectPage.tsx
@@ -5,7 +5,6 @@ import type {ListItem} from '@components/SelectionList/types';
import SelectionScreen from '@components/SelectionScreen';
import Text from '@components/Text';
import useLocalize from '@hooks/useLocalize';
-import usePermissions from '@hooks/usePermissions';
import useThemeStyles from '@hooks/useThemeStyles';
import * as QuickbooksDesktop from '@libs/actions/connections/QuickbooksDesktop';
import * as ErrorUtils from '@libs/ErrorUtils';
@@ -39,8 +38,6 @@ function QuickbooksDesktopExportDateSelectPage({policy}: WithPolicyConnectionsPr
[exportDate, translate],
);
- const {canUseNewDotQBD} = usePermissions();
-
const selectExportDate = useCallback(
(row: CardListItem) => {
if (row.value !== exportDate) {
@@ -54,7 +51,7 @@ function QuickbooksDesktopExportDateSelectPage({policy}: WithPolicyConnectionsPr
return (
mode.isSelected)?.keyForList}
title="workspace.qbd.exportDate.label"
- shouldBeBlocked={!canUseNewDotQBD} // TODO: remove it once the QBD beta is done
connectionName={CONST.POLICY.CONNECTIONS.NAME.QBD}
pendingAction={PolicyUtils.settingsPendingAction([CONST.QUICKBOOKS_DESKTOP_CONFIG.EXPORT_DATE], qbdConfig?.pendingFields)}
errors={ErrorUtils.getLatestErrorField(qbdConfig, CONST.QUICKBOOKS_DESKTOP_CONFIG.EXPORT_DATE)}
diff --git a/src/pages/workspace/accounting/qbd/export/QuickbooksDesktopExportPage.tsx b/src/pages/workspace/accounting/qbd/export/QuickbooksDesktopExportPage.tsx
index 72d4c75b761c..8555be0d3d83 100644
--- a/src/pages/workspace/accounting/qbd/export/QuickbooksDesktopExportPage.tsx
+++ b/src/pages/workspace/accounting/qbd/export/QuickbooksDesktopExportPage.tsx
@@ -5,7 +5,6 @@ import OfflineWithFeedback from '@components/OfflineWithFeedback';
import Text from '@components/Text';
import TextLink from '@components/TextLink';
import useLocalize from '@hooks/useLocalize';
-import usePermissions from '@hooks/usePermissions';
import useThemeStyles from '@hooks/useThemeStyles';
import * as PolicyUtils from '@libs/PolicyUtils';
import Navigation from '@navigation/Navigation';
@@ -22,7 +21,6 @@ function QuickbooksDesktopExportPage({policy}: WithPolicyConnectionsProps) {
const policyOwner = policy?.owner ?? '';
const qbdConfig = policy?.connections?.quickbooksDesktop?.config;
const errorFields = qbdConfig?.errorFields;
- const {canUseNewDotQBD} = usePermissions();
const shouldShowVendorMenuItems = useMemo(
() => qbdConfig?.export?.nonReimbursable === CONST.QUICKBOOKS_DESKTOP_NON_REIMBURSABLE_EXPORT_ACCOUNT_TYPE.VENDOR_BILL,
@@ -77,14 +75,13 @@ function QuickbooksDesktopExportPage({policy}: WithPolicyConnectionsProps) {
displayName={QuickbooksDesktopExportPage.displayName}
headerTitle="workspace.accounting.export"
title="workspace.qbd.exportDescription"
- accessVariants={[CONST.POLICY.ACCESS_VARIANTS.ADMIN]}
+ accessVariants={[CONST.POLICY.ACCESS_VARIANTS.ADMIN, CONST.POLICY.ACCESS_VARIANTS.CONTROL]}
policyID={policyID}
featureName={CONST.POLICY.MORE_FEATURES.ARE_CONNECTIONS_ENABLED}
contentContainerStyle={styles.pb2}
titleStyle={styles.ph5}
- shouldBeBlocked={!canUseNewDotQBD} // TODO: [QBD] remove it once the QBD beta is done
connectionName={CONST.POLICY.CONNECTIONS.NAME.QBD}
- onBackButtonPress={() => Navigation.goBack(ROUTES.POLICY_ACCOUNTING.getRoute(policyID))}
+ onBackButtonPress={() => Navigation.goBack()}
>
{menuItems.map((menuItem) => (
{
@@ -69,13 +67,12 @@ function QuickbooksDesktopNonReimbursableDefaultVendorSelectPage({policy}: WithP
return (
mode.isSelected)?.keyForList}
diff --git a/src/pages/workspace/accounting/qbd/export/QuickbooksDesktopOutOfPocketExpenseAccountSelectPage.tsx b/src/pages/workspace/accounting/qbd/export/QuickbooksDesktopOutOfPocketExpenseAccountSelectPage.tsx
index 73665cb3823b..86b30b52142f 100644
--- a/src/pages/workspace/accounting/qbd/export/QuickbooksDesktopOutOfPocketExpenseAccountSelectPage.tsx
+++ b/src/pages/workspace/accounting/qbd/export/QuickbooksDesktopOutOfPocketExpenseAccountSelectPage.tsx
@@ -6,7 +6,6 @@ import type {ListItem} from '@components/SelectionList/types';
import SelectionScreen from '@components/SelectionScreen';
import Text from '@components/Text';
import useLocalize from '@hooks/useLocalize';
-import usePermissions from '@hooks/usePermissions';
import useThemeStyles from '@hooks/useThemeStyles';
import * as QuickbooksDesktop from '@libs/actions/connections/QuickbooksDesktop';
import * as ErrorUtils from '@libs/ErrorUtils';
@@ -30,8 +29,6 @@ function QuickbooksDesktopOutOfPocketExpenseAccountSelectPage({policy}: WithPoli
const styles = useThemeStyles();
const qbdConfig = policy?.connections?.quickbooksDesktop?.config;
- const {canUseNewDotQBD} = usePermissions();
-
const [title, description] = useMemo(() => {
let titleText: TranslationPaths | undefined;
let descriptionText: string | undefined;
@@ -94,7 +91,7 @@ function QuickbooksDesktopOutOfPocketExpenseAccountSelectPage({policy}: WithPoli
return (
mode.isSelected)?.keyForList}
title={title}
- shouldBeBlocked={!canUseNewDotQBD} // TODO: [QBD] remove it once the QBD beta is done
connectionName={CONST.POLICY.CONNECTIONS.NAME.QBD}
pendingAction={PolicyUtils.settingsPendingAction([CONST.QUICKBOOKS_DESKTOP_CONFIG.REIMBURSABLE_ACCOUNT], qbdConfig?.pendingFields)}
errors={ErrorUtils.getLatestErrorField(qbdConfig, CONST.QUICKBOOKS_DESKTOP_CONFIG.REIMBURSABLE_ACCOUNT)}
diff --git a/src/pages/workspace/accounting/qbd/export/QuickbooksDesktopOutOfPocketExpenseConfigurationPage.tsx b/src/pages/workspace/accounting/qbd/export/QuickbooksDesktopOutOfPocketExpenseConfigurationPage.tsx
index f12d70afc0e7..08092465159b 100644
--- a/src/pages/workspace/accounting/qbd/export/QuickbooksDesktopOutOfPocketExpenseConfigurationPage.tsx
+++ b/src/pages/workspace/accounting/qbd/export/QuickbooksDesktopOutOfPocketExpenseConfigurationPage.tsx
@@ -4,7 +4,6 @@ import ConnectionLayout from '@components/ConnectionLayout';
import MenuItemWithTopDescription from '@components/MenuItemWithTopDescription';
import OfflineWithFeedback from '@components/OfflineWithFeedback';
import useLocalize from '@hooks/useLocalize';
-import usePermissions from '@hooks/usePermissions';
import useThemeStyles from '@hooks/useThemeStyles';
import * as QuickbooksDesktop from '@libs/actions/connections/QuickbooksDesktop';
import * as ErrorUtils from '@libs/ErrorUtils';
@@ -40,7 +39,6 @@ function QuickbooksDesktopOutOfPocketExpenseConfigurationPage({policy}: WithPoli
const policyID = policy?.id ?? '-1';
const qbdConfig = policy?.connections?.quickbooksDesktop?.config;
const reimbursable = qbdConfig?.export.reimbursable;
- const {canUseNewDotQBD} = usePermissions();
const [exportHintText, accountDescription, accountsList] = useMemo(() => {
let hintText: string | undefined;
let description: string | undefined;
@@ -90,12 +88,11 @@ function QuickbooksDesktopOutOfPocketExpenseConfigurationPage({policy}: WithPoli
displayName={QuickbooksDesktopOutOfPocketExpenseConfigurationPage.displayName}
headerTitle="workspace.accounting.exportOutOfPocket"
title="workspace.qbd.exportOutOfPocketExpensesDescription"
- accessVariants={[CONST.POLICY.ACCESS_VARIANTS.ADMIN]}
+ accessVariants={[CONST.POLICY.ACCESS_VARIANTS.ADMIN, CONST.POLICY.ACCESS_VARIANTS.CONTROL]}
policyID={policyID}
featureName={CONST.POLICY.MORE_FEATURES.ARE_CONNECTIONS_ENABLED}
contentContainerStyle={styles.pb2}
titleStyle={styles.ph5}
- shouldBeBlocked={!canUseNewDotQBD} // TODO: [QBD] remove it once the QBD beta is done
connectionName={CONST.POLICY.CONNECTIONS.NAME.QBD}
onBackButtonPress={() => Navigation.goBack(ROUTES.POLICY_ACCOUNTING_QUICKBOOKS_DESKTOP_EXPORT.getRoute(policyID))}
>
diff --git a/src/pages/workspace/accounting/qbd/export/QuickbooksDesktopOutOfPocketExpenseEntitySelectPage.tsx b/src/pages/workspace/accounting/qbd/export/QuickbooksDesktopOutOfPocketExpenseEntitySelectPage.tsx
index a9e29db7c88c..930d26f0a796 100644
--- a/src/pages/workspace/accounting/qbd/export/QuickbooksDesktopOutOfPocketExpenseEntitySelectPage.tsx
+++ b/src/pages/workspace/accounting/qbd/export/QuickbooksDesktopOutOfPocketExpenseEntitySelectPage.tsx
@@ -4,7 +4,6 @@ import type {ListItem} from '@components/SelectionList/types';
import SelectionScreen from '@components/SelectionScreen';
import type {SelectorType} from '@components/SelectionScreen';
import useLocalize from '@hooks/useLocalize';
-import usePermissions from '@hooks/usePermissions';
import useThemeStyles from '@hooks/useThemeStyles';
import * as QuickbooksDesktop from '@libs/actions/connections/QuickbooksDesktop';
import * as ErrorUtils from '@libs/ErrorUtils';
@@ -30,7 +29,6 @@ function QuickbooksDesktopOutOfPocketExpenseEntitySelectPage({policy}: WithPolic
const reimbursable = qbdConfig?.export.reimbursable;
const hasErrors = !!qbdConfig?.errorFields?.reimbursable;
const policyID = policy?.id ?? '-1';
- const {canUseNewDotQBD} = usePermissions();
const data: MenuItem[] = useMemo(
() => [
@@ -87,7 +85,7 @@ function QuickbooksDesktopOutOfPocketExpenseEntitySelectPage({policy}: WithPolic
return (
mode.isSelected)?.keyForList}
title="workspace.accounting.exportAs"
- shouldBeBlocked={!canUseNewDotQBD} // TODO: [QBD] remove it once the QBD beta is done
connectionName={CONST.POLICY.CONNECTIONS.NAME.QBD}
pendingAction={PolicyUtils.settingsPendingAction([CONST.QUICKBOOKS_DESKTOP_CONFIG.REIMBURSABLE, CONST.QUICKBOOKS_DESKTOP_CONFIG.REIMBURSABLE_ACCOUNT], qbdConfig?.pendingFields)}
errors={
diff --git a/src/pages/workspace/accounting/qbd/export/QuickbooksDesktopPreferredExporterConfigurationPage.tsx b/src/pages/workspace/accounting/qbd/export/QuickbooksDesktopPreferredExporterConfigurationPage.tsx
index 00b57ca4fd9c..eef48ee04dcf 100644
--- a/src/pages/workspace/accounting/qbd/export/QuickbooksDesktopPreferredExporterConfigurationPage.tsx
+++ b/src/pages/workspace/accounting/qbd/export/QuickbooksDesktopPreferredExporterConfigurationPage.tsx
@@ -5,7 +5,6 @@ import SelectionScreen from '@components/SelectionScreen';
import Text from '@components/Text';
import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails';
import useLocalize from '@hooks/useLocalize';
-import usePermissions from '@hooks/usePermissions';
import useThemeStyles from '@hooks/useThemeStyles';
import * as QuickbooksDesktop from '@libs/actions/connections/QuickbooksDesktop';
import * as ErrorUtils from '@libs/ErrorUtils';
@@ -28,7 +27,6 @@ function QuickbooksDesktopPreferredExporterConfigurationPage({policy}: WithPolic
const qbdConfig = policy?.connections?.quickbooksDesktop?.config;
const exporters = getAdminEmployees(policy);
const {login: currentUserLogin} = useCurrentUserPersonalDetails();
- const {canUseNewDotQBD} = usePermissions();
const currentExporter = qbdConfig?.export?.exporter;
const policyID = policy?.id ?? '-1';
@@ -77,7 +75,7 @@ function QuickbooksDesktopPreferredExporterConfigurationPage({policy}: WithPolic
return (
mode.isSelected)?.keyForList}
title="workspace.accounting.preferredExporter"
- shouldBeBlocked={!canUseNewDotQBD} // TODO: [QBD] Remove it once the QBD beta is done
connectionName={CONST.POLICY.CONNECTIONS.NAME.QBD}
pendingAction={PolicyUtils.settingsPendingAction([CONST.QUICKBOOKS_DESKTOP_CONFIG.EXPORTER], qbdConfig?.pendingFields)}
errors={ErrorUtils.getLatestErrorField(qbdConfig, CONST.QUICKBOOKS_DESKTOP_CONFIG.EXPORTER)}
diff --git a/src/pages/workspace/accounting/qbd/import/QuickbooksDesktopChartOfAccountsPage.tsx b/src/pages/workspace/accounting/qbd/import/QuickbooksDesktopChartOfAccountsPage.tsx
index 8e6cd895c435..45335ad759be 100644
--- a/src/pages/workspace/accounting/qbd/import/QuickbooksDesktopChartOfAccountsPage.tsx
+++ b/src/pages/workspace/accounting/qbd/import/QuickbooksDesktopChartOfAccountsPage.tsx
@@ -3,7 +3,6 @@ import ConnectionLayout from '@components/ConnectionLayout';
import MenuItemWithTopDescription from '@components/MenuItemWithTopDescription';
import Text from '@components/Text';
import useLocalize from '@hooks/useLocalize';
-import usePermissions from '@hooks/usePermissions';
import useThemeStyles from '@hooks/useThemeStyles';
import * as QuickbooksDesktop from '@libs/actions/connections/QuickbooksDesktop';
import * as ErrorUtils from '@libs/ErrorUtils';
@@ -20,7 +19,6 @@ function QuickbooksDesktopChartOfAccountsPage({policy}: WithPolicyProps) {
const {translate} = useLocalize();
const styles = useThemeStyles();
const policyID = policy?.id ?? '-1';
- const {canUseNewDotQBD} = usePermissions();
const qbdConfig = policy?.connections?.quickbooksDesktop?.config;
return (
@@ -29,11 +27,10 @@ function QuickbooksDesktopChartOfAccountsPage({policy}: WithPolicyProps) {
displayName={QuickbooksDesktopChartOfAccountsPage.displayName}
headerTitle="workspace.accounting.accounts"
title="workspace.qbd.accountsDescription"
- accessVariants={[CONST.POLICY.ACCESS_VARIANTS.ADMIN]}
+ accessVariants={[CONST.POLICY.ACCESS_VARIANTS.ADMIN, CONST.POLICY.ACCESS_VARIANTS.CONTROL]}
featureName={CONST.POLICY.MORE_FEATURES.ARE_CONNECTIONS_ENABLED}
contentContainerStyle={[styles.pb2, styles.ph5]}
connectionName={CONST.POLICY.CONNECTIONS.NAME.QBD}
- shouldBeBlocked={!canUseNewDotQBD} // TODO: [QBD] Will be removed when release
onBackButtonPress={() => Navigation.goBack(ROUTES.POLICY_ACCOUNTING_QUICKBOOKS_DESKTOP_IMPORT.getRoute(policyID))}
>
mode.isSelected)?.keyForList}
title="workspace.common.displayedAs"
- shouldBeBlocked={!canUseNewDotQBD} // TODO: [QBD] remove it once the QBD beta is done
connectionName={CONST.POLICY.CONNECTIONS.NAME.QBD}
pendingAction={PolicyUtils.settingsPendingAction([CONST.QUICKBOOKS_DESKTOP_CONFIG.MAPPINGS.CLASSES], qbdConfig?.pendingFields)}
errors={ErrorUtils.getLatestErrorField(qbdConfig, CONST.QUICKBOOKS_DESKTOP_CONFIG.MAPPINGS.CLASSES)}
diff --git a/src/pages/workspace/accounting/qbd/import/QuickbooksDesktopClassesPage.tsx b/src/pages/workspace/accounting/qbd/import/QuickbooksDesktopClassesPage.tsx
index b480b442bd88..6ce56c44c680 100644
--- a/src/pages/workspace/accounting/qbd/import/QuickbooksDesktopClassesPage.tsx
+++ b/src/pages/workspace/accounting/qbd/import/QuickbooksDesktopClassesPage.tsx
@@ -3,7 +3,6 @@ import ConnectionLayout from '@components/ConnectionLayout';
import MenuItemWithTopDescription from '@components/MenuItemWithTopDescription';
import OfflineWithFeedback from '@components/OfflineWithFeedback';
import useLocalize from '@hooks/useLocalize';
-import usePermissions from '@hooks/usePermissions';
import useThemeStyles from '@hooks/useThemeStyles';
import * as QuickbooksDesktop from '@libs/actions/connections/QuickbooksDesktop';
import * as ErrorUtils from '@libs/ErrorUtils';
@@ -19,7 +18,6 @@ import ROUTES from '@src/ROUTES';
function QuickbooksDesktopClassesPage({policy}: WithPolicyProps) {
const {translate} = useLocalize();
const styles = useThemeStyles();
- const {canUseNewDotQBD} = usePermissions();
const policyID = policy?.id ?? '-1';
const qbdConfig = policy?.connections?.quickbooksDesktop?.config;
const isSwitchOn = !!(qbdConfig?.mappings?.classes && qbdConfig.mappings.classes !== CONST.INTEGRATION_ENTITY_MAP_TYPES.NONE);
@@ -30,11 +28,10 @@ function QuickbooksDesktopClassesPage({policy}: WithPolicyProps) {
displayName={QuickbooksDesktopClassesPage.displayName}
headerTitle="workspace.qbd.classes"
title="workspace.qbd.classesDescription"
- accessVariants={[CONST.POLICY.ACCESS_VARIANTS.ADMIN]}
+ accessVariants={[CONST.POLICY.ACCESS_VARIANTS.ADMIN, CONST.POLICY.ACCESS_VARIANTS.CONTROL]}
policyID={policyID}
featureName={CONST.POLICY.MORE_FEATURES.ARE_CONNECTIONS_ENABLED}
contentContainerStyle={[styles.pb2, styles.ph5]}
- shouldBeBlocked={!canUseNewDotQBD} // TODO: [QBD] Will be removed when release
connectionName={CONST.POLICY.CONNECTIONS.NAME.QBD}
onBackButtonPress={() => Navigation.goBack(ROUTES.POLICY_ACCOUNTING_QUICKBOOKS_DESKTOP_IMPORT.getRoute(policyID))}
>
diff --git a/src/pages/workspace/accounting/qbd/import/QuickbooksDesktopCustomersDisplayedAsPage.tsx b/src/pages/workspace/accounting/qbd/import/QuickbooksDesktopCustomersDisplayedAsPage.tsx
index a4b0a94bfd5e..4d088ed67e8d 100644
--- a/src/pages/workspace/accounting/qbd/import/QuickbooksDesktopCustomersDisplayedAsPage.tsx
+++ b/src/pages/workspace/accounting/qbd/import/QuickbooksDesktopCustomersDisplayedAsPage.tsx
@@ -3,7 +3,6 @@ import RadioListItem from '@components/SelectionList/RadioListItem';
import type {ListItem} from '@components/SelectionList/types';
import SelectionScreen from '@components/SelectionScreen';
import useLocalize from '@hooks/useLocalize';
-import usePermissions from '@hooks/usePermissions';
import useThemeStyles from '@hooks/useThemeStyles';
import * as QuickbooksDesktop from '@libs/actions/connections/QuickbooksDesktop';
import * as ErrorUtils from '@libs/ErrorUtils';
@@ -21,7 +20,6 @@ type CardListItem = ListItem & {
function QuickbooksDesktopCustomersDisplayedAsPage({policy}: WithPolicyConnectionsProps) {
const {translate} = useLocalize();
- const {canUseNewDotQBD} = usePermissions();
const styles = useThemeStyles();
const policyID = policy?.id ?? '-1';
const qbdConfig = policy?.connections?.quickbooksDesktop?.config;
@@ -56,7 +54,7 @@ function QuickbooksDesktopCustomersDisplayedAsPage({policy}: WithPolicyConnectio
return (
mode.isSelected)?.keyForList}
title="workspace.common.displayedAs"
- shouldBeBlocked={!canUseNewDotQBD} // TODO: [QBD] remove it once the QBD beta is done
connectionName={CONST.POLICY.CONNECTIONS.NAME.QBD}
pendingAction={PolicyUtils.settingsPendingAction([CONST.QUICKBOOKS_DESKTOP_CONFIG.MAPPINGS.CUSTOMERS], qbdConfig?.pendingFields)}
errors={ErrorUtils.getLatestErrorField(qbdConfig, CONST.QUICKBOOKS_DESKTOP_CONFIG.MAPPINGS.CUSTOMERS)}
diff --git a/src/pages/workspace/accounting/qbd/import/QuickbooksDesktopCustomersPage.tsx b/src/pages/workspace/accounting/qbd/import/QuickbooksDesktopCustomersPage.tsx
index e58826a12703..3f0300dc2bf4 100644
--- a/src/pages/workspace/accounting/qbd/import/QuickbooksDesktopCustomersPage.tsx
+++ b/src/pages/workspace/accounting/qbd/import/QuickbooksDesktopCustomersPage.tsx
@@ -3,7 +3,6 @@ import ConnectionLayout from '@components/ConnectionLayout';
import MenuItemWithTopDescription from '@components/MenuItemWithTopDescription';
import OfflineWithFeedback from '@components/OfflineWithFeedback';
import useLocalize from '@hooks/useLocalize';
-import usePermissions from '@hooks/usePermissions';
import useThemeStyles from '@hooks/useThemeStyles';
import * as QuickbooksDesktop from '@libs/actions/connections/QuickbooksDesktop';
import * as ErrorUtils from '@libs/ErrorUtils';
@@ -19,7 +18,6 @@ import ROUTES from '@src/ROUTES';
function QuickbooksDesktopCustomersPage({policy}: WithPolicyProps) {
const {translate} = useLocalize();
const styles = useThemeStyles();
- const {canUseNewDotQBD} = usePermissions();
const policyID = policy?.id ?? '-1';
const qbdConfig = policy?.connections?.quickbooksDesktop?.config;
const isSwitchOn = !!(qbdConfig?.mappings?.customers && qbdConfig.mappings.customers !== CONST.INTEGRATION_ENTITY_MAP_TYPES.NONE);
@@ -30,11 +28,10 @@ function QuickbooksDesktopCustomersPage({policy}: WithPolicyProps) {
displayName={QuickbooksDesktopCustomersPage.displayName}
headerTitle="workspace.qbd.customers"
title="workspace.qbd.customersDescription"
- accessVariants={[CONST.POLICY.ACCESS_VARIANTS.ADMIN]}
+ accessVariants={[CONST.POLICY.ACCESS_VARIANTS.ADMIN, CONST.POLICY.ACCESS_VARIANTS.CONTROL]}
policyID={policyID}
featureName={CONST.POLICY.MORE_FEATURES.ARE_CONNECTIONS_ENABLED}
contentContainerStyle={[styles.pb2, styles.ph5]}
- shouldBeBlocked={!canUseNewDotQBD} // TODO: [QBD] Will be removed when release
connectionName={CONST.POLICY.CONNECTIONS.NAME.QBD}
onBackButtonPress={() => Navigation.goBack(ROUTES.POLICY_ACCOUNTING_QUICKBOOKS_DESKTOP_IMPORT.getRoute(policyID))}
>
diff --git a/src/pages/workspace/accounting/qbd/import/QuickbooksDesktopImportPage.tsx b/src/pages/workspace/accounting/qbd/import/QuickbooksDesktopImportPage.tsx
index 1f8a4dde2bbd..c83ec9f85b1b 100644
--- a/src/pages/workspace/accounting/qbd/import/QuickbooksDesktopImportPage.tsx
+++ b/src/pages/workspace/accounting/qbd/import/QuickbooksDesktopImportPage.tsx
@@ -3,7 +3,6 @@ import ConnectionLayout from '@components/ConnectionLayout';
import MenuItemWithTopDescription from '@components/MenuItemWithTopDescription';
import OfflineWithFeedback from '@components/OfflineWithFeedback';
import useLocalize from '@hooks/useLocalize';
-import usePermissions from '@hooks/usePermissions';
import useThemeStyles from '@hooks/useThemeStyles';
import * as PolicyUtils from '@libs/PolicyUtils';
import Navigation from '@navigation/Navigation';
@@ -24,7 +23,6 @@ function QuickbooksDesktopImportPage({policy}: WithPolicyProps) {
const styles = useThemeStyles();
const policyID = policy?.id ?? '-1';
const {mappings, pendingFields, errorFields} = policy?.connections?.quickbooksDesktop?.config ?? {};
- const {canUseNewDotQBD} = usePermissions();
const sections: QBDSectionType[] = [
{
@@ -57,12 +55,11 @@ function QuickbooksDesktopImportPage({policy}: WithPolicyProps) {
displayName={QuickbooksDesktopImportPage.displayName}
headerTitle="workspace.accounting.import"
title="workspace.qbd.importDescription"
- accessVariants={[CONST.POLICY.ACCESS_VARIANTS.ADMIN]}
+ accessVariants={[CONST.POLICY.ACCESS_VARIANTS.ADMIN, CONST.POLICY.ACCESS_VARIANTS.CONTROL]}
policyID={policyID}
featureName={CONST.POLICY.MORE_FEATURES.ARE_CONNECTIONS_ENABLED}
contentContainerStyle={styles.pb2}
titleStyle={styles.ph5}
- shouldBeBlocked={!canUseNewDotQBD} // TODO: [QBD] Will be removed when release
connectionName={CONST.POLICY.CONNECTIONS.NAME.QBD}
onBackButtonPress={() => Navigation.goBack(ROUTES.POLICY_ACCOUNTING.getRoute(policyID))}
>
diff --git a/src/pages/workspace/accounting/qbd/import/QuickbooksDesktopItemsPage.tsx b/src/pages/workspace/accounting/qbd/import/QuickbooksDesktopItemsPage.tsx
index c73ae2d8e754..1bb2af69eda1 100644
--- a/src/pages/workspace/accounting/qbd/import/QuickbooksDesktopItemsPage.tsx
+++ b/src/pages/workspace/accounting/qbd/import/QuickbooksDesktopItemsPage.tsx
@@ -1,7 +1,6 @@
import React from 'react';
import ConnectionLayout from '@components/ConnectionLayout';
import useLocalize from '@hooks/useLocalize';
-import usePermissions from '@hooks/usePermissions';
import useThemeStyles from '@hooks/useThemeStyles';
import * as QuickbooksDesktop from '@libs/actions/connections/QuickbooksDesktop';
import * as ErrorUtils from '@libs/ErrorUtils';
@@ -17,7 +16,6 @@ import ROUTES from '@src/ROUTES';
function QuickbooksDesktopItemsPage({policy}: WithPolicyProps) {
const {translate} = useLocalize();
const styles = useThemeStyles();
- const {canUseNewDotQBD} = usePermissions();
const policyID = policy?.id ?? '-1';
const qbdConfig = policy?.connections?.quickbooksDesktop?.config;
@@ -26,11 +24,10 @@ function QuickbooksDesktopItemsPage({policy}: WithPolicyProps) {
displayName={QuickbooksDesktopItemsPage.displayName}
headerTitle="workspace.qbd.items"
title="workspace.qbd.itemsDescription"
- accessVariants={[CONST.POLICY.ACCESS_VARIANTS.ADMIN]}
+ accessVariants={[CONST.POLICY.ACCESS_VARIANTS.ADMIN, CONST.POLICY.ACCESS_VARIANTS.CONTROL]}
policyID={policyID}
featureName={CONST.POLICY.MORE_FEATURES.ARE_CONNECTIONS_ENABLED}
contentContainerStyle={[styles.pb2, styles.ph5]}
- shouldBeBlocked={!canUseNewDotQBD} // TODO: [QBD] Will be removed when release
connectionName={CONST.POLICY.CONNECTIONS.NAME.QBD}
onBackButtonPress={() => Navigation.goBack(ROUTES.POLICY_ACCOUNTING_QUICKBOOKS_DESKTOP_IMPORT.getRoute(policyID))}
>
diff --git a/src/pages/workspace/accounting/qbo/export/QuickbooksExportConfigurationPage.tsx b/src/pages/workspace/accounting/qbo/export/QuickbooksExportConfigurationPage.tsx
index 67208b2ce8b0..5ad55aea810c 100644
--- a/src/pages/workspace/accounting/qbo/export/QuickbooksExportConfigurationPage.tsx
+++ b/src/pages/workspace/accounting/qbo/export/QuickbooksExportConfigurationPage.tsx
@@ -82,7 +82,7 @@ function QuickbooksExportConfigurationPage({policy}: WithPolicyConnectionsProps)
contentContainerStyle={styles.pb2}
titleStyle={styles.ph5}
connectionName={CONST.POLICY.CONNECTIONS.NAME.QBO}
- onBackButtonPress={() => Navigation.goBack(ROUTES.POLICY_ACCOUNTING.getRoute(policyID))}
+ onBackButtonPress={() => Navigation.goBack()}
>
{menuItems.map((menuItem) => (
)}
- {!!paymentBankAccountID && isContinuousReconciliationOn && (
+ {!!paymentBankAccountID && !!isContinuousReconciliationOn && (
Navigation.navigate(ROUTES.POLICY_ACCOUNTING_QUICKBOOKS_DESKTOP_EXPORT.getRoute(policyID)),
onCardReconciliationPagePress: () => {},
onAdvancedPagePress: () => Navigation.navigate(ROUTES.WORKSPACE_ACCOUNTING_QUICKBOOKS_DESKTOP_ADVANCED.getRoute(policyID)),
- // TODO: [QBD] Make sure all values are passed to subscribedSettings
subscribedImportSettings: [
CONST.QUICKBOOKS_DESKTOP_CONFIG.ENABLE_NEW_CATEGORIES,
CONST.QUICKBOOKS_DESKTOP_CONFIG.MAPPINGS.CLASSES,
diff --git a/src/pages/workspace/accounting/xero/XeroTrackingCategoryConfigurationPage.tsx b/src/pages/workspace/accounting/xero/XeroTrackingCategoryConfigurationPage.tsx
index 1e9f7382f51f..91096880c938 100644
--- a/src/pages/workspace/accounting/xero/XeroTrackingCategoryConfigurationPage.tsx
+++ b/src/pages/workspace/accounting/xero/XeroTrackingCategoryConfigurationPage.tsx
@@ -61,7 +61,7 @@ function XeroTrackingCategoryConfigurationPage({policy}: WithPolicyProps) {
errors={ErrorUtils.getLatestErrorField(xeroConfig ?? {}, CONST.XERO_CONFIG.IMPORT_TRACKING_CATEGORIES)}
onCloseError={() => Policy.clearXeroErrorField(policyID, CONST.XERO_CONFIG.IMPORT_TRACKING_CATEGORIES)}
/>
- {xeroConfig?.importTrackingCategories && (
+ {!!xeroConfig?.importTrackingCategories && (
{menuItems.map((menuItem) => (
Policy.clearXeroErrorField(policyID, CONST.XERO_CONFIG.SYNC_REIMBURSED_REPORTS)}
/>
- {sync?.syncReimbursedReports && (
+ {!!sync?.syncReimbursedReports && (
<>
- {policy?.areRulesEnabled && (
+ {!!policy?.areRulesEnabled && (
<>
{translate('workspace.rules.categoryRules.title')}
@@ -250,7 +250,7 @@ function CategorySettingsPage({
- {policyCategory?.areCommentsRequired && (
+ {!!policyCategory?.areCommentsRequired && (
)}
- {canUseCategoryAndTagApprovers && (
+ {!!canUseCategoryAndTagApprovers && (
<>
)}
- {policy?.tax?.trackingEnabled && (
+ {!!policy?.tax?.trackingEnabled && (
- {canUseWorkspaceRules && !!currentPolicy && (sections.at(0)?.data?.length ?? 0) > 0 && (
+ {!!canUseWorkspaceRules && !!currentPolicy && (sections.at(0)?.data?.length ?? 0) > 0 && (
@@ -140,7 +140,7 @@ function WorkspaceCategoriesSettingsPage({policy, route}: WorkspaceCategoriesSet
}}
/>
)}
- {canUseWorkspaceRules && categoryID && groupID && (
+ {!!canUseWorkspaceRules && !!categoryID && !!groupID && (
- {exportMenuItem?.description && (
+ {!!exportMenuItem?.description && (
{translate('workspace.moreFeatures.companyCards.integrationExportTitleFirstPart', {integration: exportMenuItem.description})}{' '}
- {exportMenuItem && (
+ {!!exportMenuItem && (
Navigation.navigate(ROUTES.WORKSPACE_COMPANY_CARD_NAME.getRoute(policyID, cardID, bank))}
/>
- {exportMenuItem && (
+ {!!exportMenuItem && (
Navigation.navigate(ROUTES.WORKSPACE_COMPANY_CARDS_SELECT_FEED.getRoute(policyID))}
style={[styles.flexRow, styles.alignItemsCenter, styles.gap3, shouldChangeLayout && styles.mb3]}
- accessibilityLabel={feedName}
+ accessibilityLabel={formattedFeedName}
>
- {feedName}
+ {formattedFeedName}
{PolicyUtils.hasPolicyFeedsError(cardFeeds?.settings?.companyCards ?? {}, selectedFeed) && (
- {url && (
+ {!!url && (
diff --git a/src/pages/workspace/companyCards/addNew/DetailsStep.tsx b/src/pages/workspace/companyCards/addNew/DetailsStep.tsx
index 2c719acd47d8..c4b427578a52 100644
--- a/src/pages/workspace/companyCards/addNew/DetailsStep.tsx
+++ b/src/pages/workspace/companyCards/addNew/DetailsStep.tsx
@@ -104,6 +104,7 @@ function DetailsStep({policyID}: DetailsStepProps) {
inputID={INPUT_IDS.PROCESSOR_ID}
label={translate('workspace.companyCards.addNewCard.feedDetails.vcf.processorLabel')}
role={CONST.ROLE.PRESENTATION}
+ maxLength={CONST.STANDARD_LENGTH_LIMIT}
containerStyles={[styles.mb6]}
ref={inputCallbackRef}
/>
@@ -112,6 +113,7 @@ function DetailsStep({policyID}: DetailsStepProps) {
inputID={INPUT_IDS.BANK_ID}
label={translate('workspace.companyCards.addNewCard.feedDetails.vcf.bankLabel')}
role={CONST.ROLE.PRESENTATION}
+ maxLength={CONST.STANDARD_LENGTH_LIMIT}
containerStyles={[styles.mb6]}
/>
>
@@ -130,6 +133,7 @@ function DetailsStep({policyID}: DetailsStepProps) {
inputID={INPUT_IDS.DISTRIBUTION_ID}
label={translate('workspace.companyCards.addNewCard.feedDetails.cdf.distributionLabel')}
role={CONST.ROLE.PRESENTATION}
+ maxLength={CONST.STANDARD_LENGTH_LIMIT}
containerStyles={[styles.mb6]}
ref={inputCallbackRef}
/>
@@ -141,6 +145,7 @@ function DetailsStep({policyID}: DetailsStepProps) {
inputID={INPUT_IDS.DELIVERY_FILE_NAME}
label={translate('workspace.companyCards.addNewCard.feedDetails.gl1025.fileNameLabel')}
role={CONST.ROLE.PRESENTATION}
+ maxLength={CONST.STANDARD_LENGTH_LIMIT}
containerStyles={[styles.mb6]}
ref={inputCallbackRef}
/>
diff --git a/src/pages/workspace/distanceRates/PolicyDistanceRatesSettingsPage.tsx b/src/pages/workspace/distanceRates/PolicyDistanceRatesSettingsPage.tsx
index 8e32f9a992af..f9918ff6612d 100644
--- a/src/pages/workspace/distanceRates/PolicyDistanceRatesSettingsPage.tsx
+++ b/src/pages/workspace/distanceRates/PolicyDistanceRatesSettingsPage.tsx
@@ -101,7 +101,7 @@ function PolicyDistanceRatesSettingsPage({route}: PolicyDistanceRatesSettingsPag
keyboardShouldPersistTaps="always"
>
- {defaultUnit && (
+ {!!defaultUnit && (
)}
- {policy?.areCategoriesEnabled && OptionsListUtils.hasEnabledOptions(policyCategories ?? {}) && (
+ {!!policy?.areCategoriesEnabled && OptionsListUtils.hasEnabledOptions(policyCategories ?? {}) && (
@@ -27,17 +32,22 @@ function EmptyCardView() {
{translate('workspace.expensifyCard.disclaimer')}
diff --git a/src/pages/workspace/expensifyCard/WorkspaceExpensifyCardBankAccounts.tsx b/src/pages/workspace/expensifyCard/WorkspaceExpensifyCardBankAccounts.tsx
index b8b8776df048..f296b16c13f6 100644
--- a/src/pages/workspace/expensifyCard/WorkspaceExpensifyCardBankAccounts.tsx
+++ b/src/pages/workspace/expensifyCard/WorkspaceExpensifyCardBankAccounts.tsx
@@ -2,9 +2,13 @@ import type {StackScreenProps} from '@react-navigation/stack';
import React from 'react';
import {View} from 'react-native';
import {useOnyx} from 'react-native-onyx';
+import BlockingView from '@components/BlockingViews/BlockingView';
+import Button from '@components/Button';
import HeaderWithBackButton from '@components/HeaderWithBackButton';
import getBankIcon from '@components/Icon/BankIcons';
import * as Expensicons from '@components/Icon/Expensicons';
+import * as Illustrations from '@components/Icon/Illustrations';
+import LottieAnimations from '@components/LottieAnimations';
import MenuItem from '@components/MenuItem';
import ScreenWrapper from '@components/ScreenWrapper';
import Text from '@components/Text';
@@ -16,6 +20,7 @@ import * as PolicyUtils from '@libs/PolicyUtils';
import Navigation from '@navigation/Navigation';
import type {SettingsNavigatorParamList} from '@navigation/types';
import AccessOrNotFoundWrapper from '@pages/workspace/AccessOrNotFoundWrapper';
+import variables from '@styles/variables';
import * as Card from '@userActions/Card';
import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
@@ -35,14 +40,30 @@ function WorkspaceExpensifyCardBankAccounts({route}: WorkspaceExpensifyCardBankA
const workspaceAccountID = PolicyUtils.getWorkspaceAccountID(policyID);
+ const [cardSettings] = useOnyx(`${ONYXKEYS.COLLECTION.PRIVATE_EXPENSIFY_CARD_SETTINGS}${workspaceAccountID}`);
+ const [cardOnWaitlist] = useOnyx(`${ONYXKEYS.COLLECTION.NVP_EXPENSIFY_ON_CARD_WAITLIST}${policyID}`);
+
+ const getVerificationState = () => {
+ if (cardOnWaitlist) {
+ return CONST.EXPENSIFY_CARD.VERIFICATION_STATE.ON_WAITLIST;
+ }
+ if (cardSettings?.isSuccess) {
+ return CONST.EXPENSIFY_CARD.VERIFICATION_STATE.VERIFIED;
+ }
+
+ if (cardSettings?.isLoading) {
+ return CONST.EXPENSIFY_CARD.VERIFICATION_STATE.LOADING;
+ }
+
+ return '';
+ };
+
const handleAddBankAccount = () => {
Navigation.navigate(ROUTES.BANK_ACCOUNT_WITH_STEP_TO_OPEN.getRoute('new', policyID, ROUTES.WORKSPACE_EXPENSIFY_CARD.getRoute(policyID)));
};
const handleSelectBankAccount = (value?: number) => {
Card.configureExpensifyCardsForPolicy(policyID, value);
- Card.updateSettlementAccount(workspaceAccountID, policyID, value);
- Navigation.navigate(ROUTES.WORKSPACE_EXPENSIFY_CARD_ISSUE_NEW.getRoute(policyID));
};
const renderBankOptions = () => {
@@ -75,6 +96,82 @@ function WorkspaceExpensifyCardBankAccounts({route}: WorkspaceExpensifyCardBankA
});
};
+ const verificationState = getVerificationState();
+ const isInVerificationState = !!verificationState;
+
+ const renderVerificationStateView = () => {
+ switch (verificationState) {
+ case CONST.EXPENSIFY_CARD.VERIFICATION_STATE.LOADING:
+ return (
+
+ );
+ case CONST.EXPENSIFY_CARD.VERIFICATION_STATE.ON_WAITLIST:
+ return (
+ <>
+
+
-
+
);
}
diff --git a/src/pages/workspace/expensifyCard/issueNew/LimitStep.tsx b/src/pages/workspace/expensifyCard/issueNew/LimitStep.tsx
index 87b8eb39edcb..f3e8bd5c7b12 100644
--- a/src/pages/workspace/expensifyCard/issueNew/LimitStep.tsx
+++ b/src/pages/workspace/expensifyCard/issueNew/LimitStep.tsx
@@ -1,13 +1,10 @@
import React, {useCallback} from 'react';
-import {View} from 'react-native';
import {useOnyx} from 'react-native-onyx';
import AmountForm from '@components/AmountForm';
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 InteractiveStepSubHeader from '@components/InteractiveStepSubHeader';
-import ScreenWrapper from '@components/ScreenWrapper';
+import InteractiveStepWrapper from '@components/InteractiveStepWrapper';
import Text from '@components/Text';
import useAutoFocusInput from '@hooks/useAutoFocusInput';
import useLocalize from '@hooks/useLocalize';
@@ -63,22 +60,15 @@ function LimitStep() {
);
return (
-
-
-
-
- {translate('workspace.card.issueNewCard.setLimit')}
-
+
);
}
diff --git a/src/pages/workspace/expensifyCard/issueNew/LimitTypeStep.tsx b/src/pages/workspace/expensifyCard/issueNew/LimitTypeStep.tsx
index 5dff1e9b5109..a593fb4c75c7 100644
--- a/src/pages/workspace/expensifyCard/issueNew/LimitTypeStep.tsx
+++ b/src/pages/workspace/expensifyCard/issueNew/LimitTypeStep.tsx
@@ -1,11 +1,8 @@
import React, {useCallback, useMemo, useState} from 'react';
-import {View} from 'react-native';
import type {OnyxEntry} from 'react-native-onyx';
import {useOnyx} from 'react-native-onyx';
import Button from '@components/Button';
-import HeaderWithBackButton from '@components/HeaderWithBackButton';
-import InteractiveStepSubHeader from '@components/InteractiveStepSubHeader';
-import ScreenWrapper from '@components/ScreenWrapper';
+import InteractiveStepWrapper from '@components/InteractiveStepWrapper';
import SelectionList from '@components/SelectionList';
import RadioListItem from '@components/SelectionList/RadioListItem';
import Text from '@components/Text';
@@ -84,22 +81,15 @@ function LimitTypeStep({policy}: LimitTypeStepProps) {
}, [areApprovalsConfigured, translate, typeSelected]);
return (
-
-
-
-
- {translate('workspace.card.issueNewCard.chooseLimitType')}
-
+
);
}
diff --git a/src/pages/workspace/invoices/WorkspaceInvoicesPage.tsx b/src/pages/workspace/invoices/WorkspaceInvoicesPage.tsx
index 4f14950ec93d..0697ac0750cd 100644
--- a/src/pages/workspace/invoices/WorkspaceInvoicesPage.tsx
+++ b/src/pages/workspace/invoices/WorkspaceInvoicesPage.tsx
@@ -37,9 +37,9 @@ function WorkspaceInvoicesPage({route}: WorkspaceInvoicesPageProps) {
>
{(_hasVBA?: boolean, policyID?: string) => (
- {policyID && }
- {policyID && }
- {policyID && }
+ {!!policyID && }
+ {!!policyID && }
+ {!!policyID && }
)}
diff --git a/src/pages/workspace/invoices/WorkspaceInvoicingDetailsName.tsx b/src/pages/workspace/invoices/WorkspaceInvoicingDetailsName.tsx
index 80f323431baa..2b933a4ab695 100644
--- a/src/pages/workspace/invoices/WorkspaceInvoicingDetailsName.tsx
+++ b/src/pages/workspace/invoices/WorkspaceInvoicingDetailsName.tsx
@@ -14,6 +14,7 @@ import * as ValidationUtils from '@libs/ValidationUtils';
import Navigation from '@navigation/Navigation';
import type {SettingsNavigatorParamList} from '@navigation/types';
import AccessOrNotFoundWrapper from '@pages/workspace/AccessOrNotFoundWrapper';
+import * as Policy from '@userActions/Policy/Policy';
import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
import type SCREENS from '@src/SCREENS';
@@ -31,7 +32,7 @@ function WorkspaceInvoicingDetailsName({route}: WorkspaceInvoicingDetailsNamePro
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const submit = (values: FormOnyxValues) => {
- // TODO: implement UpdateInvoiceCompanyName API call when it's supported
+ Policy.updateInvoiceCompanyName(policyID, values[INPUT_IDS.COMPANY_NAME]);
Navigation.goBack();
};
diff --git a/src/pages/workspace/invoices/WorkspaceInvoicingDetailsWebsite.tsx b/src/pages/workspace/invoices/WorkspaceInvoicingDetailsWebsite.tsx
index f5d0cd1b6819..7c9ed9b2eb8b 100644
--- a/src/pages/workspace/invoices/WorkspaceInvoicingDetailsWebsite.tsx
+++ b/src/pages/workspace/invoices/WorkspaceInvoicingDetailsWebsite.tsx
@@ -16,6 +16,7 @@ import * as ValidationUtils from '@libs/ValidationUtils';
import Navigation from '@navigation/Navigation';
import type {SettingsNavigatorParamList} from '@navigation/types';
import AccessOrNotFoundWrapper from '@pages/workspace/AccessOrNotFoundWrapper';
+import * as Policy from '@userActions/Policy/Policy';
import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
import type SCREENS from '@src/SCREENS';
@@ -33,7 +34,7 @@ function WorkspaceInvoicingDetailsWebsite({route}: WorkspaceInvoicingDetailsWebs
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const submit = (values: FormOnyxValues) => {
- // TODO: implement UpdateInvoiceCompanyWebsite API call when it's supported
+ Policy.updateInvoiceCompanyWebsite(policyID, values[INPUT_IDS.COMPANY_WEBSITE]);
Navigation.goBack();
};
diff --git a/src/pages/workspace/members/WorkspaceOwnerChangeWrapperPage.tsx b/src/pages/workspace/members/WorkspaceOwnerChangeWrapperPage.tsx
index 325e1112ab4b..c6a37c668c1a 100644
--- a/src/pages/workspace/members/WorkspaceOwnerChangeWrapperPage.tsx
+++ b/src/pages/workspace/members/WorkspaceOwnerChangeWrapperPage.tsx
@@ -75,7 +75,7 @@ function WorkspaceOwnerChangeWrapperPage({route, policy}: WorkspaceOwnerChangeWr
}}
/>
- {policy?.isLoading && }
+ {!!policy?.isLoading && }
{shouldShowPaymentCardForm && }
{!policy?.isLoading && !shouldShowPaymentCardForm && (
- {policy?.areRulesEnabled && canUseCategoryAndTagApprovers && !isMultiLevelTags && (
+ {!!policy?.areRulesEnabled && !!canUseCategoryAndTagApprovers && !isMultiLevelTags && (
<>
{translate('workspace.tags.tagRules')}
diff --git a/src/pages/workspace/tags/WorkspaceTagsSettingsPage.tsx b/src/pages/workspace/tags/WorkspaceTagsSettingsPage.tsx
index 3fadba088648..eb9b89ee5679 100644
--- a/src/pages/workspace/tags/WorkspaceTagsSettingsPage.tsx
+++ b/src/pages/workspace/tags/WorkspaceTagsSettingsPage.tsx
@@ -106,7 +106,7 @@ function WorkspaceTagsSettingsPage({route}: WorkspaceTagsSettingsPageProps) {
/>
- {canUseWorkspaceRules && policy?.areRulesEnabled && (
+ {!!canUseWorkspaceRules && !!policy?.areRulesEnabled && (
{translate('workspace.tags.trackBillable')}
diff --git a/src/pages/workspace/taxes/WorkspaceEditTaxPage.tsx b/src/pages/workspace/taxes/WorkspaceEditTaxPage.tsx
index 0f50ed8fe0b8..f10e0fba84a2 100644
--- a/src/pages/workspace/taxes/WorkspaceEditTaxPage.tsx
+++ b/src/pages/workspace/taxes/WorkspaceEditTaxPage.tsx
@@ -158,7 +158,7 @@ function WorkspaceEditTaxPage({
}}
/>
- {shouldShowDeleteMenuItem && (
+ {!!shouldShowDeleteMenuItem && (