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. - -
    -
  1. Under the Accounting settings for your workspace, click Import under the QuickBooks Online connection.
  2. -
  3. Review each of the following import settings:
  4. -
      -
    • 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. - -
    -
  1. Under the Accounting settings for your workspace, click Export under the QuickBooks Online connection.
  2. -
  3. Review each of the following export settings:
  4. -
      -
    • 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. - -
    -
  1. Under the Accounting settings for your workspace, click Advanced under the QuickBooks Online connection.
  2. -
  3. Select an option for each of the following settings:
  4. -
      -
    • 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. + +
    +
  1. Under the Accounting settings for your workspace, click Import under the Xero connection.
  2. +
  3. Select an option for each of the following settings to determine what information will be imported from Xero into Expensify:
  4. +
      +
    • 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. + +
    +
  1. Under the Accounting settings for your workspace, click Export under the Xero connection.
  2. +
  3. Review each of the following export settings:
  4. +
      +
    • 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. + +
    +
  1. Under the Accounting settings for your workspace, click Advanced under the Xero connection.
  2. +
  3. Select an option for each of the following settings:
  4. +
      +
    • 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 @@ CFBundlePackageType APPL CFBundleShortVersionString - 9.0.54 + 9.0.55 CFBundleSignature ???? CFBundleURLTypes @@ -40,7 +40,7 @@ CFBundleVersion - 9.0.54.11 + 9.0.55.9 FullStory OrgId 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 @@ CFBundlePackageType BNDL CFBundleShortVersionString - 9.0.54 + 9.0.55 CFBundleSignature ???? 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.55 CFBundleVersion - 9.0.54.11 + 9.0.55.9 NSExtension NSExtensionPointIdentifier 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