diff --git a/.env.example b/.env.example index f398a72aa0af..c4adc4f98b65 100644 --- a/.env.example +++ b/.env.example @@ -11,6 +11,7 @@ USE_WEB_PROXY=false USE_WDYR=false CAPTURE_METRICS=false ONYX_METRICS=false +USE_THIRD_PARTY_SCRIPTS=false EXPENSIFY_ACCOUNT_ID_ACCOUNTING=-1 EXPENSIFY_ACCOUNT_ID_ADMIN=-1 diff --git a/.env.production b/.env.production index bb925eb70d39..cca9adf26f52 100644 --- a/.env.production +++ b/.env.production @@ -8,6 +8,6 @@ USE_WEB_PROXY=false ENVIRONMENT=production SEND_CRASH_REPORTS=true -FB_API_KEY=AIzaSyDxzigVLZl4G8MP7jACQ0qpmADMzmrrON0 -FB_APP_ID=1:921154746561:web:1583e882584cf151027c40 -FB_PROJECT_ID=expensify-chat +FB_API_KEY=AIzaSyBrLKgCuo6Vem6Xi5RPokdumssW8HaWBow +FB_APP_ID=1:1008697809946:web:08de4ecb7656b7235445a3 +FB_PROJECT_ID=expensify-mobile-app 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/Internal.md b/.github/ISSUE_TEMPLATE/Internal.md new file mode 100644 index 000000000000..c4fde407df13 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/Internal.md @@ -0,0 +1,25 @@ +--- +name: Open an internal issue for a backend fix +about: Use this template to report a backend issue that an internal Expensify employee needs to fix +labels: Hot Pick, Daily, Internal, AutoAssignerNewDotQuality +--- + + +**Original GH:** + +## Action Performed: +Break down in numbered steps + +## Expected Result: +Describe what you think the backend _SHOULD_ have done + +## Actual Result: +Describe what the backend _ACTUALLY_ did + +## Screenshots/Videos + +
+ Add any screenshot/video evidence + + +
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/binoculars.svg b/assets/images/binoculars.svg new file mode 100644 index 000000000000..64977dee38b5 --- /dev/null +++ b/assets/images/binoculars.svg @@ -0,0 +1,25 @@ + + + + + + + + + + + + + \ 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.common.ts b/config/webpack/webpack.common.ts index 2d8e27fd453e..ab5c304fcd1e 100644 --- a/config/webpack/webpack.common.ts +++ b/config/webpack/webpack.common.ts @@ -11,6 +11,8 @@ import {BundleAnalyzerPlugin} from 'webpack-bundle-analyzer'; import CustomVersionFilePlugin from './CustomVersionFilePlugin'; import type Environment from './types'; +dotenv.config(); + type Options = { rel: string; as: string; @@ -82,6 +84,7 @@ const getCommonConfiguration = ({file = '.env', platform = 'web'}: Environment): isWeb: platform === 'web', isProduction: file === '.env.production', isStaging: file === '.env.staging', + useThirdPartyScripts: process.env.USE_THIRD_PARTY_SCRIPTS === 'true' || (platform === 'web' && file === '.env.production'), }), new PreloadWebpackPlugin({ rel: 'preload', 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/bank-accounts-and-payments/payments/Reimburse-Reports.md b/docs/articles/expensify-classic/bank-accounts-and-payments/payments/Reimburse-Reports.md index afe366fb1dbe..41dc52a4239c 100644 --- a/docs/articles/expensify-classic/bank-accounts-and-payments/payments/Reimburse-Reports.md +++ b/docs/articles/expensify-classic/bank-accounts-and-payments/payments/Reimburse-Reports.md @@ -14,7 +14,7 @@ Before a report can be reimbursed via direct deposit: To reimburse a report via direct deposit (USD): 1. Open the report. -2. Click the **Reimburse** button and select **Via Direct Deposit (ACH)**. +2. Click the **Reimburse** button and select **Via Direct Deposit**. 3. Confirm that the correct bank account is listed in the dropdown menu. 4. Click **Accept Terms & Pay**. @@ -27,7 +27,7 @@ Before a report can be reimbursed via global reimbursement: To reimburse a report via global reimbursement: 1. Open the report. -2. Click the **Reimburse** button and select **Via Direct Deposit (ACH)**. +2. Click the **Reimburse** button and select **Via Direct Deposit**. 3. Confirm that the correct bank account is listed in the dropdown menu. 4. Click **Accept Terms & Pay**. diff --git a/docs/articles/expensify-classic/connections/quickbooks-desktop/Connect-To-QuickBooks-Desktop.md b/docs/articles/expensify-classic/connections/quickbooks-desktop/Connect-To-QuickBooks-Desktop.md index bda84eb0a49f..30785330a9ad 100644 --- a/docs/articles/expensify-classic/connections/quickbooks-desktop/Connect-To-QuickBooks-Desktop.md +++ b/docs/articles/expensify-classic/connections/quickbooks-desktop/Connect-To-QuickBooks-Desktop.md @@ -52,6 +52,10 @@ For this step, it is key to ensure that the correct company file is open in Quic ![The Web Connector pop-up, where you will need to click "Yes"](https://help.expensify.com/assets/images/QBO_desktop_07.png){:width="100%"} +{% include info.html %} +Be sure to securely save this password in a trusted password manager. You'll need it for future configuration updates or troubleshooting. Having it easily accessible will help avoid delays and ensure a smoother workflow. +{% include end-info.html %} + # FAQ ## What are the hardware and software requirements for the QuickBooks Desktop connector? 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..fde2c43e9d95 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_02.png){:width="100%"} ## How to Receive an Invoice Payment in Expensify diff --git a/docs/articles/expensify-classic/workspaces/Personal-and-Corporate-Karma.md b/docs/articles/expensify-classic/workspaces/Personal-and-Corporate-Karma.md index 5c146b279163..ca6d9cf52f47 100644 --- a/docs/articles/expensify-classic/workspaces/Personal-and-Corporate-Karma.md +++ b/docs/articles/expensify-classic/workspaces/Personal-and-Corporate-Karma.md @@ -23,6 +23,16 @@ For every $500 of expenses added, you’ll donate $1 to a related Expensify.org The fund from your Personal Karma is determined by the expense's MCC (Merchant Category Code). Each MCC supports one of Expensify.org's funds: Climate Justice, Food Security, Housing Equity, Reentry Services, and Youth Advocacy. +## How do I opt-in to Personal Karma donations? + +You can enable Personal Karma donations from your personal workspace settings. + +- Sign in to your account at www.expensify.com. +- Go to **Settings** > **Workspaces** > click on your **Individual** workspace settings. +- Click Opt-in to Karma donations. + +![Settings > Workspaces > Individual workspace > enable Personal Karma in settings](https://help.expensify.com/assets/images/ExpensifyHelp_OldDot_Karma_Individual.png){:width="100%"} + ## What is Corporate Karma? Corporate Karma is for companies that want to engage in social responsibility. Each month, the donation is calculated based on the total amount of all approved expense reports, including invoices, across all Workspace. @@ -31,12 +41,12 @@ For every $500 your team spends monthly, your company will donate $1 to a relate The fund to which your Corporate Karma goes is determined by the expense's MCC (Merchant Category Code). Each MCC supports one of Expensify.org's funds: Climate Justice, Food Security, Housing Equity, Reentry Services, and Youth Advocacy. -{% include faq-begin.md %} - -**How do I opt-in to Personal or Corporate Karma donations?** +## How do I opt-in to Corporate Karma donations? -You can donate Personal and Corporate Karma to Expensify.org in your company or personal workspace settings. +As a [workspace billing owner](https://help.expensify.com/articles/expensify-classic/workspaces/Assign-billing-owner-and-payment-account), you can enable Corporate Karma from the group workspace settings. -Go to **Settings** > **Workspaces** > click on your Individual or Group workspace settings and Opt-in to Karma donations. +- Sign in to your account at www.expensify.com. +- Go to **Settings** > **Workspaces** > **Subscription**. +- Toggle on Karma donations. -{% include faq-end.md %} +![Settings > Workspaces > Group > enable Corporate Karma in subscription settings](https://help.expensify.com/assets/images/ExpensifyHelp_OldDot_Karma_Group.png){:width="100%"} 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/assets/images/ExpensifyHelp_OldDot_Karma_Group.png b/docs/assets/images/ExpensifyHelp_OldDot_Karma_Group.png new file mode 100644 index 000000000000..e0d5d406ba2f Binary files /dev/null and b/docs/assets/images/ExpensifyHelp_OldDot_Karma_Group.png differ diff --git a/docs/assets/images/ExpensifyHelp_OldDot_Karma_Individual.png b/docs/assets/images/ExpensifyHelp_OldDot_Karma_Individual.png new file mode 100644 index 000000000000..d3115469350f Binary files /dev/null and b/docs/assets/images/ExpensifyHelp_OldDot_Karma_Individual.png differ 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/Appfile b/fastlane/Appfile index 43c8da9bddd5..42f887a827d1 100644 --- a/fastlane/Appfile +++ b/fastlane/Appfile @@ -1,9 +1,4 @@ -app_identifier("com.chat.expensify.chat") # The bundle identifier of your app -apple_id("ios@expensify.com") # Your Apple email address - -itc_team_id("152696") # App Store Connect Team ID -team_id("368M544MTT") # Developer Portal Team ID - -for_lane :build_hybrid, :build_unsigned_hybrid, :upload_testflight_hybrid do - app_identifier("com.expensify.expensifylite") -end +# See https://docs.fastlane.tools/advanced/Appfile/ +apple_id("ios@expensify.com") +itc_team_id("152696") +team_id("368M544MTT") diff --git a/fastlane/Fastfile b/fastlane/Fastfile index 3c85aa5ff3d1..e90fdbe50255 100644 --- a/fastlane/Fastfile +++ b/fastlane/Fastfile @@ -191,6 +191,25 @@ platform :android do track: 'alpha', rollout: '1.0' ) + + # Update the internal testing group "beta" with the latest version + upload_to_play_store( + package_name: "org.me.mobiexpensifyg", + json_key: './android-fastlane-json-key.json', + track: 'alpha', + track_promote_to: 'beta', + skip_upload_aab: true + ) + + # Update the internal testing group "Internal Testers" with the latest version + upload_to_play_store( + package_name: "org.me.mobiexpensifyg", + json_key: './android-fastlane-json-key.json', + track: 'alpha', + track_promote_to: 'Internal Testers', + skip_upload_aab: true + ) + end desc "Deploy app to Google Play production" @@ -369,6 +388,7 @@ platform :ios do desc "Upload app to TestFlight" lane :upload_testflight do upload_to_testflight( + app_identifier: "com.chat.expensify.chat", api_key_path: "./ios/ios-fastlane-json-key.json", distribute_external: true, notify_external_testers: true, @@ -402,11 +422,13 @@ platform :ios do desc "Upload HybridApp to TestFlight" lane :upload_testflight_hybrid do upload_to_testflight( + app_identifier: "com.expensify.expensifylite", api_key_path: "./ios/ios-fastlane-json-key.json", distribute_external: true, notify_external_testers: true, + reject_build_waiting_for_review: 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"], @@ -433,9 +455,10 @@ platform :ios do ) end - desc "Submit app to App Store Review" + desc "Submit app for production App Store Review" lane :submit_for_review do deliver( + app_identifier: "com.chat.expensify.chat", api_key_path: "./ios/ios-fastlane-json-key.json", # Skip HTMl report verification diff --git a/ios/GoogleService-Info-DEV.plist b/ios/GoogleService-Info-DEV.plist new file mode 100644 index 000000000000..5bfb1a332dfc --- /dev/null +++ b/ios/GoogleService-Info-DEV.plist @@ -0,0 +1,38 @@ + + + + + CLIENT_ID + 921154746561-8niu5ba8g4dgsqsqso3lugdhe6vikqpq.apps.googleusercontent.com + REVERSED_CLIENT_ID + com.googleusercontent.apps.921154746561-8niu5ba8g4dgsqsqso3lugdhe6vikqpq + ANDROID_CLIENT_ID + 921154746561-cbegir0tnc2gan6k1gre5vtn75p60hom.apps.googleusercontent.com + API_KEY + AIzaSyA9Qn7q5Iw26gTzjI7012C4PaFrFagpC_I + GCM_SENDER_ID + 921154746561 + PLIST_VERSION + 1 + BUNDLE_ID + com.expensify.chat.dev + PROJECT_ID + expensify-chat + STORAGE_BUCKET + expensify-chat.appspot.com + IS_ADS_ENABLED + + IS_ANALYTICS_ENABLED + + IS_APPINVITE_ENABLED + + IS_GCM_ENABLED + + IS_SIGNIN_ENABLED + + GOOGLE_APP_ID + 1:921154746561:ios:12c3a0b9276d7d2f027c40 + DATABASE_URL + https://expensify-chat.firebaseio.com + + diff --git a/ios/GoogleService-Info.plist b/ios/GoogleService-Info.plist index 147bec8c2875..e8549ed328fd 100644 --- a/ios/GoogleService-Info.plist +++ b/ios/GoogleService-Info.plist @@ -6,6 +6,8 @@ 921154746561-s3uqn2oe4m85tufi6mqflbfbuajrm2i3.apps.googleusercontent.com REVERSED_CLIENT_ID com.googleusercontent.apps.921154746561-s3uqn2oe4m85tufi6mqflbfbuajrm2i3 + ANDROID_CLIENT_ID + 921154746561-cbegir0tnc2gan6k1gre5vtn75p60hom.apps.googleusercontent.com API_KEY AIzaSyA9Qn7q5Iw26gTzjI7012C4PaFrFagpC_I GCM_SENDER_ID @@ -21,7 +23,7 @@ IS_ADS_ENABLED IS_ANALYTICS_ENABLED - + IS_APPINVITE_ENABLED IS_GCM_ENABLED @@ -33,4 +35,4 @@ DATABASE_URL https://expensify-chat.firebaseio.com - \ No newline at end of file + diff --git a/ios/NewExpensify.xcodeproj/project.pbxproj b/ios/NewExpensify.xcodeproj/project.pbxproj index b3ec8febb1df..d8eceab72b95 100644 --- a/ios/NewExpensify.xcodeproj/project.pbxproj +++ b/ios/NewExpensify.xcodeproj/project.pbxproj @@ -18,7 +18,6 @@ 0CDA8E38287DD6A0004ECBEC /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 0CDA8E36287DD6A0004ECBEC /* Images.xcassets */; }; 0DFC45942C884E0A00B56C91 /* RCTShortcutManagerModule.m in Sources */ = {isa = PBXBuildFile; fileRef = 0DFC45932C884E0A00B56C91 /* RCTShortcutManagerModule.m */; }; 0DFC45952C884E0A00B56C91 /* RCTShortcutManagerModule.m in Sources */ = {isa = PBXBuildFile; fileRef = 0DFC45932C884E0A00B56C91 /* RCTShortcutManagerModule.m */; }; - 0F5BE0CE252686330097D869 /* GoogleService-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = 0F5BE0CD252686320097D869 /* GoogleService-Info.plist */; }; 0F5E5350263B73FD004CA14F /* EnvironmentChecker.m in Sources */ = {isa = PBXBuildFile; fileRef = 0F5E534F263B73FD004CA14F /* EnvironmentChecker.m */; }; 0F5E5351263B73FD004CA14F /* EnvironmentChecker.m in Sources */ = {isa = PBXBuildFile; fileRef = 0F5E534F263B73FD004CA14F /* EnvironmentChecker.m */; }; 1246A3EF20E54E7A9494C8B9 /* ExpensifyNeue-Regular.otf in Resources */ = {isa = PBXBuildFile; fileRef = F4F8A052A22040339996324B /* ExpensifyNeue-Regular.otf */; }; @@ -34,6 +33,9 @@ 70CF6E82262E297300711ADC /* BootSplash.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 70CF6E81262E297300711ADC /* BootSplash.storyboard */; }; 7F5E81F06BCCF61AD02CEA06 /* ExpoModulesProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = BCD444BEDDB0AF1745B39049 /* ExpoModulesProvider.swift */; }; 7F9DD8DA2B2A445B005E3AFA /* ExpError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7F9DD8D92B2A445B005E3AFA /* ExpError.swift */; }; + 7FB680AE2CC94EDA006693CF /* GoogleService-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = 7FB680AD2CC94EDA006693CF /* GoogleService-Info.plist */; }; + 7FB680AF2CC94EDA006693CF /* GoogleService-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = 7FB680AD2CC94EDA006693CF /* GoogleService-Info.plist */; }; + 7FB680B02CC94EDA006693CF /* GoogleService-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = 7FB680AD2CC94EDA006693CF /* GoogleService-Info.plist */; }; 7FD73C9E2B23CE9500420AF3 /* NotificationService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7FD73C9D2B23CE9500420AF3 /* NotificationService.swift */; }; 7FD73CA22B23CE9500420AF3 /* NotificationServiceExtension.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = 7FD73C9B2B23CE9500420AF3 /* NotificationServiceExtension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; 8744C5400E24E379441C04A4 /* libPods-NewExpensify.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 59A21B2405370FDDD847C813 /* libPods-NewExpensify.a */; }; @@ -43,7 +45,7 @@ D27CE6B77196EF3EF450EEAC /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = 0D3F9E814828D91464DF9D35 /* PrivacyInfo.xcprivacy */; }; DD79042B2792E76D004484B4 /* RCTBootSplash.mm in Sources */ = {isa = PBXBuildFile; fileRef = DD79042A2792E76D004484B4 /* RCTBootSplash.mm */; }; DDCB2E57F334C143AC462B43 /* ExpoModulesProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4D20D83B0E39BA6D21761E72 /* ExpoModulesProvider.swift */; }; - E51DC681C7DEE40AEBDDFBFE /* BuildFile in Frameworks */ = {isa = PBXBuildFile; }; + E51DC681C7DEE40AEBDDFBFE /* (null) in Frameworks */ = {isa = PBXBuildFile; }; E9DF872D2525201700607FDC /* AirshipConfig.plist in Resources */ = {isa = PBXBuildFile; fileRef = E9DF872C2525201700607FDC /* AirshipConfig.plist */; }; ED222ED90E074A5481A854FA /* ExpensifyNeue-BoldItalic.otf in Resources */ = {isa = PBXBuildFile; fileRef = 8B28D84EF339436DBD42A203 /* ExpensifyNeue-BoldItalic.otf */; }; F0C450EA2705020500FD2970 /* colors.json in Resources */ = {isa = PBXBuildFile; fileRef = F0C450E92705020500FD2970 /* colors.json */; }; @@ -94,7 +96,6 @@ 0D3F9E814828D91464DF9D35 /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xml; name = PrivacyInfo.xcprivacy; path = NewExpensify/PrivacyInfo.xcprivacy; sourceTree = ""; }; 0DFC45922C884D7900B56C91 /* RCTShortcutManagerModule.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = RCTShortcutManagerModule.h; sourceTree = ""; }; 0DFC45932C884E0A00B56C91 /* RCTShortcutManagerModule.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = RCTShortcutManagerModule.m; sourceTree = ""; }; - 0F5BE0CD252686320097D869 /* GoogleService-Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = "GoogleService-Info.plist"; sourceTree = ""; }; 0F5E534E263B73D5004CA14F /* EnvironmentChecker.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = EnvironmentChecker.h; sourceTree = ""; }; 0F5E534F263B73FD004CA14F /* EnvironmentChecker.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = EnvironmentChecker.m; sourceTree = ""; }; 13B07F961A680F5B00A75B9A /* New Expensify Dev.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "New Expensify Dev.app"; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -133,6 +134,7 @@ 7F3784A72C75131000063508 /* NewExpensifyReleaseProduction.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; name = NewExpensifyReleaseProduction.entitlements; path = NewExpensify/NewExpensifyReleaseProduction.entitlements; sourceTree = ""; }; 7F9C91352CA5EC4900FC4DC1 /* NotificationServiceExtension.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = NotificationServiceExtension.entitlements; sourceTree = ""; }; 7F9DD8D92B2A445B005E3AFA /* ExpError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExpError.swift; sourceTree = ""; }; + 7FB680AD2CC94EDA006693CF /* GoogleService-Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = "GoogleService-Info.plist"; sourceTree = ""; }; 7FD73C9B2B23CE9500420AF3 /* NotificationServiceExtension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = NotificationServiceExtension.appex; sourceTree = BUILT_PRODUCTS_DIR; }; 7FD73C9D2B23CE9500420AF3 /* NotificationService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationService.swift; sourceTree = ""; }; 7FD73C9F2B23CE9500420AF3 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; @@ -176,8 +178,8 @@ buildActionMask = 2147483647; files = ( 383643682B6D4AE2005BB9AE /* DeviceCheck.framework in Frameworks */, - E51DC681C7DEE40AEBDDFBFE /* BuildFile in Frameworks */, - E51DC681C7DEE40AEBDDFBFE /* BuildFile in Frameworks */, + E51DC681C7DEE40AEBDDFBFE /* (null) in Frameworks */, + E51DC681C7DEE40AEBDDFBFE /* (null) in Frameworks */, 8744C5400E24E379441C04A4 /* libPods-NewExpensify.a in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; @@ -212,13 +214,13 @@ 13B07FAE1A68108700A75B9A /* NewExpensify */ = { isa = PBXGroup; children = ( + 7FB680AD2CC94EDA006693CF /* GoogleService-Info.plist */, 7F3784A72C75131000063508 /* NewExpensifyReleaseProduction.entitlements */, 7F3784A62C7512D900063508 /* NewExpensifyReleaseAdHoc.entitlements */, 7F3784A52C7512CF00063508 /* NewExpensifyReleaseDevelopment.entitlements */, 7F3784A42C7512BF00063508 /* NewExpensifyDebugProduction.entitlements */, 7F3784A32C75129D00063508 /* NewExpensifyDebugAdHoc.entitlements */, 7F3784A22C75103800063508 /* NewExpensifyDebugDevelopment.entitlements */, - 0F5BE0CD252686320097D869 /* GoogleService-Info.plist */, E9DF872C2525201700607FDC /* AirshipConfig.plist */, 0CDA8E36287DD6A0004ECBEC /* Images.xcassets */, 008F07F21AC5B25A0029DE68 /* main.jsbundle */, @@ -500,6 +502,7 @@ buildActionMask = 2147483647; files = ( 0CDA8E38287DD6A0004ECBEC /* Images.xcassets in Resources */, + 7FB680B02CC94EDA006693CF /* GoogleService-Info.plist in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -507,7 +510,6 @@ isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( - 0F5BE0CE252686330097D869 /* GoogleService-Info.plist in Resources */, E9DF872D2525201700607FDC /* AirshipConfig.plist in Resources */, F0C450EA2705020500FD2970 /* colors.json in Resources */, 083353EB2B5AB22A00C603C0 /* attention.mp3 in Resources */, @@ -519,6 +521,7 @@ 083353EE2B5AB22A00C603C0 /* success.mp3 in Resources */, 0C7C65547D7346EB923BE808 /* ExpensifyMono-Regular.otf in Resources */, 2A9F8CDA983746B0B9204209 /* ExpensifyNeue-Bold.otf in Resources */, + 7FB680AE2CC94EDA006693CF /* GoogleService-Info.plist in Resources */, 083353EC2B5AB22A00C603C0 /* done.mp3 in Resources */, 083353ED2B5AB22A00C603C0 /* receive.mp3 in Resources */, ED222ED90E074A5481A854FA /* ExpensifyNeue-BoldItalic.otf in Resources */, @@ -532,6 +535,7 @@ isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( + 7FB680AF2CC94EDA006693CF /* GoogleService-Info.plist in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/ios/NewExpensify.xcodeproj/xcshareddata/xcschemes/New Expensify Dev.xcscheme b/ios/NewExpensify.xcodeproj/xcshareddata/xcschemes/New Expensify Dev.xcscheme index 93d775217f11..f9acbe8abe4f 100644 --- a/ios/NewExpensify.xcodeproj/xcshareddata/xcschemes/New Expensify Dev.xcscheme +++ b/ios/NewExpensify.xcodeproj/xcshareddata/xcschemes/New Expensify Dev.xcscheme @@ -60,6 +60,16 @@ ReferencedContainer = "container:NewExpensify.xcodeproj"> + + + + + + CFBundlePackageType APPL CFBundleShortVersionString - 9.0.55 + 9.0.57 CFBundleSignature ???? CFBundleURLTypes @@ -40,7 +40,7 @@ CFBundleVersion - 9.0.55.0 + 9.0.57.3 FullStory OrgId diff --git a/ios/NewExpensifyTests/Info.plist b/ios/NewExpensifyTests/Info.plist index eef74d4f127c..6d7ff5c4ac09 100644 --- a/ios/NewExpensifyTests/Info.plist +++ b/ios/NewExpensifyTests/Info.plist @@ -15,10 +15,10 @@ CFBundlePackageType BNDL CFBundleShortVersionString - 9.0.55 + 9.0.57 CFBundleSignature ???? CFBundleVersion - 9.0.55.0 + 9.0.57.3 diff --git a/ios/NotificationServiceExtension/Info.plist b/ios/NotificationServiceExtension/Info.plist index 33f6517f0329..605eb605529c 100644 --- a/ios/NotificationServiceExtension/Info.plist +++ b/ios/NotificationServiceExtension/Info.plist @@ -11,9 +11,9 @@ CFBundleName $(PRODUCT_NAME) CFBundleShortVersionString - 9.0.55 + 9.0.57 CFBundleVersion - 9.0.55.0 + 9.0.57.3 NSExtension NSExtensionPointIdentifier diff --git a/ios/Podfile.lock b/ios/Podfile.lock index 9a706cc4e8aa..d1851cbce1af 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -2329,10 +2329,6 @@ PODS: - ReactCommon/turbomodule/bridging - ReactCommon/turbomodule/core - Yoga - - RNDevMenu (4.1.1): - - React-Core - - React-Core/DevSupport - - React-RCTNetwork - RNFBAnalytics (12.9.3): - Firebase/Analytics (= 8.8.0) - React-Core @@ -2395,7 +2391,7 @@ PODS: - RNGoogleSignin (10.0.1): - GoogleSignIn (~> 7.0) - React-Core - - RNLiveMarkdown (0.1.176): + - RNLiveMarkdown (0.1.179): - DoubleConversion - glog - hermes-engine @@ -2415,9 +2411,9 @@ PODS: - ReactCodegen - ReactCommon/turbomodule/bridging - ReactCommon/turbomodule/core - - RNLiveMarkdown/newarch (= 0.1.176) + - RNLiveMarkdown/newarch (= 0.1.179) - Yoga - - RNLiveMarkdown/newarch (0.1.176): + - RNLiveMarkdown/newarch (0.1.179): - DoubleConversion - glog - hermes-engine @@ -2824,7 +2820,6 @@ DEPENDENCIES: - "RNCClipboard (from `../node_modules/@react-native-clipboard/clipboard`)" - "RNCPicker (from `../node_modules/@react-native-picker/picker`)" - RNDeviceInfo (from `../node_modules/react-native-device-info`) - - RNDevMenu (from `../node_modules/react-native-dev-menu`) - "RNFBAnalytics (from `../node_modules/@react-native-firebase/analytics`)" - "RNFBApp (from `../node_modules/@react-native-firebase/app`)" - "RNFBCrashlytics (from `../node_modules/@react-native-firebase/crashlytics`)" @@ -3085,8 +3080,6 @@ EXTERNAL SOURCES: :path: "../node_modules/@react-native-picker/picker" RNDeviceInfo: :path: "../node_modules/react-native-device-info" - RNDevMenu: - :path: "../node_modules/react-native-dev-menu" RNFBAnalytics: :path: "../node_modules/@react-native-firebase/analytics" RNFBApp: @@ -3263,7 +3256,6 @@ SPEC CHECKSUMS: RNCClipboard: c84275d07e3f73ff296b17e6c27e9ccdc194a0bb RNCPicker: 21ae0659666767a5c1253aef985ee5b7c527e345 RNDeviceInfo: 130237d8e97a89b68f2202d5dd18ac6bb68e7648 - RNDevMenu: 72807568fe4188bd4c40ce32675d82434b43c45d RNFBAnalytics: f76bfa164ac235b00505deb9fc1776634056898c RNFBApp: 729c0666395b1953198dc4a1ec6deb8fbe1c302e RNFBCrashlytics: 2061ca863e8e2fa1aae9b12477d7dfa8e88ca0f9 @@ -3272,7 +3264,7 @@ SPEC CHECKSUMS: RNFS: 4ac0f0ea233904cb798630b3c077808c06931688 RNGestureHandler: 8781e2529230a1bc3ea8d75e5c3cd071b6c6aed7 RNGoogleSignin: ccaa4a81582cf713eea562c5dd9dc1961a715fd0 - RNLiveMarkdown: 0b8756147a5e8eeea98d3e1187c0c27d5a96d1ff + RNLiveMarkdown: 7acba70803223c6fa369c32cd2673c415ae3b5c4 RNLocalize: d4b8af4e442d4bcca54e68fc687a2129b4d71a81 rnmapbox-maps: 460d6ff97ae49c7d5708c3212c6521697c36a0c4 RNPermissions: 0b1429b55af59d1d08b75a8be2459f65a8ac3f28 diff --git a/package-lock.json b/package-lock.json index fbdba9487652..7d7c82747222 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,17 +1,17 @@ { "name": "new.expensify", - "version": "9.0.55-0", + "version": "9.0.57-3", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "new.expensify", - "version": "9.0.55-0", + "version": "9.0.57-3", "hasInstallScript": true, "license": "MIT", "dependencies": { "@dotlottie/react-player": "^1.6.3", - "@expensify/react-native-live-markdown": "0.1.176", + "@expensify/react-native-live-markdown": "0.1.179", "@expo/metro-runtime": "~3.2.3", "@firebase/app": "^0.10.10", "@firebase/performance": "^0.6.8", @@ -51,7 +51,7 @@ "date-fns-tz": "^3.2.0", "dom-serializer": "^0.2.2", "domhandler": "^4.3.0", - "expensify-common": "2.0.100", + "expensify-common": "2.0.101", "expo": "51.0.31", "expo-av": "14.0.7", "expo-image": "1.12.15", @@ -77,11 +77,10 @@ "react-map-gl": "^7.1.3", "react-native": "0.75.2", "react-native-android-location-enabler": "^2.0.1", - "react-native-app-logs": "git+https://github.com/margelo/react-native-app-logs#7e9c311bffdc6a9eeb69d90d30ead47e01c3552a", + "react-native-app-logs": "0.3.1", "react-native-blob-util": "0.19.4", "react-native-collapsible": "^1.6.2", "react-native-config": "1.5.3", - "react-native-dev-menu": "^4.1.1", "react-native-device-info": "10.3.1", "react-native-document-picker": "^9.3.1", "react-native-draggable-flatlist": "^4.0.1", @@ -157,6 +156,7 @@ "@perf-profiler/profiler": "^0.10.10", "@perf-profiler/reporter": "^0.9.0", "@perf-profiler/types": "^0.8.0", + "@pmmmwh/react-refresh-webpack-plugin": "^0.5.15", "@react-native-community/eslint-config": "3.2.0", "@react-native/babel-preset": "0.75.2", "@react-native/metro-config": "0.75.2", @@ -219,7 +219,7 @@ "electron-builder": "25.0.0", "eslint": "^8.57.0", "eslint-config-airbnb-typescript": "^18.0.0", - "eslint-config-expensify": "^2.0.66", + "eslint-config-expensify": "^2.0.73", "eslint-config-prettier": "^9.1.0", "eslint-plugin-deprecation": "^3.0.0", "eslint-plugin-jest": "^28.6.0", @@ -252,6 +252,7 @@ "react-compiler-runtime": "^19.0.0-beta-8a03594-20241020", "react-is": "^18.3.1", "react-native-clean-project": "^4.0.0-alpha4.0", + "react-refresh": "^0.14.2", "react-test-renderer": "18.3.1", "reassure": "^1.0.0-rc.4", "semver": "7.5.2", @@ -3630,9 +3631,9 @@ } }, "node_modules/@expensify/react-native-live-markdown": { - "version": "0.1.176", - "resolved": "https://registry.npmjs.org/@expensify/react-native-live-markdown/-/react-native-live-markdown-0.1.176.tgz", - "integrity": "sha512-0IS0Rfl0qYqrE2V8jsVX58c4K/zxeNC7o1CAL9Xu+HTbTtD58Yu5gOOwp5AljkS2qdPR86swGRZyLXGkGRKkPg==", + "version": "0.1.179", + "resolved": "https://registry.npmjs.org/@expensify/react-native-live-markdown/-/react-native-live-markdown-0.1.179.tgz", + "integrity": "sha512-TzXEMPZQRBFOFquu0a9sybaDn513JnqxrfkUgqcFJZuJtvOTs6f29Aj2BG/HfDQMSnO/V3elZP1RaodBPlBMmA==", "license": "MIT", "workspaces": [ "parser", @@ -7431,6 +7432,85 @@ "node": ">=14" } }, + "node_modules/@pmmmwh/react-refresh-webpack-plugin": { + "version": "0.5.15", + "resolved": "https://registry.npmjs.org/@pmmmwh/react-refresh-webpack-plugin/-/react-refresh-webpack-plugin-0.5.15.tgz", + "integrity": "sha512-LFWllMA55pzB9D34w/wXUCf8+c+IYKuJDgxiZ3qMhl64KRMBHYM1I3VdGaD2BV5FNPV2/S2596bppxHbv2ZydQ==", + "dev": true, + "dependencies": { + "ansi-html": "^0.0.9", + "core-js-pure": "^3.23.3", + "error-stack-parser": "^2.0.6", + "html-entities": "^2.1.0", + "loader-utils": "^2.0.4", + "schema-utils": "^4.2.0", + "source-map": "^0.7.3" + }, + "engines": { + "node": ">= 10.13" + }, + "peerDependencies": { + "@types/webpack": "4.x || 5.x", + "react-refresh": ">=0.10.0 <1.0.0", + "sockjs-client": "^1.4.0", + "type-fest": ">=0.17.0 <5.0.0", + "webpack": ">=4.43.0 <6.0.0", + "webpack-dev-server": "3.x || 4.x || 5.x", + "webpack-hot-middleware": "2.x", + "webpack-plugin-serve": "0.x || 1.x" + }, + "peerDependenciesMeta": { + "@types/webpack": { + "optional": true + }, + "sockjs-client": { + "optional": true + }, + "type-fest": { + "optional": true + }, + "webpack-dev-server": { + "optional": true + }, + "webpack-hot-middleware": { + "optional": true + }, + "webpack-plugin-serve": { + "optional": true + } + } + }, + "node_modules/@pmmmwh/react-refresh-webpack-plugin/node_modules/ajv-keywords": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz", + "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==", + "dev": true, + "dependencies": { + "fast-deep-equal": "^3.1.3" + }, + "peerDependencies": { + "ajv": "^8.8.2" + } + }, + "node_modules/@pmmmwh/react-refresh-webpack-plugin/node_modules/schema-utils": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.2.0.tgz", + "integrity": "sha512-L0jRsrPpjdckP3oPug3/VxNKt2trR8TcabrM6FOAAlvC/9Phcmm+cuAgTlxBqdBR1WJx7Naj9WHw+aOmheSVbw==", + "dev": true, + "dependencies": { + "@types/json-schema": "^7.0.9", + "ajv": "^8.9.0", + "ajv-formats": "^2.1.1", + "ajv-keywords": "^5.1.0" + }, + "engines": { + "node": ">= 12.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, "node_modules/@polka/url": { "version": "1.0.0-next.25", "resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.25.tgz", @@ -17361,6 +17441,18 @@ "node": ">=6" } }, + "node_modules/ansi-html": { + "version": "0.0.9", + "resolved": "https://registry.npmjs.org/ansi-html/-/ansi-html-0.0.9.tgz", + "integrity": "sha512-ozbS3LuenHVxNRh/wdnN16QapUHzauqSomAl1jwwJRRsGwFwtj644lIhxfWu0Fy0acCij2+AEgHvjscq3dlVXg==", + "dev": true, + "engines": [ + "node >= 0.8.0" + ], + "bin": { + "ansi-html": "bin/ansi-html" + } + }, "node_modules/ansi-html-community": { "version": "0.0.8", "dev": true, @@ -20713,6 +20805,17 @@ "url": "https://opencollective.com/core-js" } }, + "node_modules/core-js-pure": { + "version": "3.38.1", + "resolved": "https://registry.npmjs.org/core-js-pure/-/core-js-pure-3.38.1.tgz", + "integrity": "sha512-BY8Etc1FZqdw1glX0XNOq2FDwfrg/VGqoZOZCdaL+UmdaqDwQwYXkMJT4t6In+zfEfOJDcM9T0KdbBeJg8KKCQ==", + "dev": true, + "hasInstallScript": true, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/core-js" + } + }, "node_modules/core-util-is": { "version": "1.0.2", "license": "MIT" @@ -22800,10 +22903,11 @@ } }, "node_modules/eslint-config-expensify": { - "version": "2.0.66", - "resolved": "https://registry.npmjs.org/eslint-config-expensify/-/eslint-config-expensify-2.0.66.tgz", - "integrity": "sha512-6L9EIAiOxZnqOcFEsIwEUmX0fvglvboyqQh7LTqy+1O2h2W3mmrMSx87ymXeyFMg1nJQtqkFnrLv5ENGS0QC3Q==", + "version": "2.0.73", + "resolved": "https://registry.npmjs.org/eslint-config-expensify/-/eslint-config-expensify-2.0.73.tgz", + "integrity": "sha512-LHHyujwjTBizm9mIQMv6g/MsAbYdeOLZrOBdFqY/LyGPUJxOr9jt22xlmTFSdKhieLrbDwkcgkXjM38Z46Nb9A==", "dev": true, + "license": "ISC", "dependencies": { "@babel/eslint-parser": "^7.25.7", "@lwc/eslint-plugin-lwc": "^1.7.2", @@ -24050,9 +24154,10 @@ } }, "node_modules/expensify-common": { - "version": "2.0.100", - "resolved": "https://registry.npmjs.org/expensify-common/-/expensify-common-2.0.100.tgz", - "integrity": "sha512-mektI+OuTywYU47Valjsn2+kLQ1/Wc9sWCY1/a0Vo8IHTXroQWvbKs5IXlkiqODi4SRonVZwOL3ha/oJD7o7nQ==", + "version": "2.0.101", + "resolved": "https://registry.npmjs.org/expensify-common/-/expensify-common-2.0.101.tgz", + "integrity": "sha512-5TStDQGsXGJjdk64PBhEdXz/3H6QDlgoanEWI076okL5un4Qd2sSRfxHRiH61foHGsswXJFIZBHK3sysKDOJ4A==", + "license": "MIT", "dependencies": { "awesome-phonenumber": "^5.4.0", "classnames": "2.5.0", @@ -34477,13 +34582,6 @@ } } }, - "node_modules/react-native-dev-menu": { - "version": "4.1.1", - "license": "MIT", - "peerDependencies": { - "react-native": ">=0.61.0" - } - }, "node_modules/react-native-device-info": { "version": "10.3.1", "license": "MIT", @@ -36745,7 +36843,8 @@ }, "node_modules/react-refresh": { "version": "0.14.2", - "license": "MIT", + "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.14.2.tgz", + "integrity": "sha512-jCvmsr+1IUSMUyzOkRcvnVbX3ZYC6g9TDrDbFuFmRDq7PD4yaGbLKNQL6k2jnArV8hjYxh7hVhAZB6s9HDGpZA==", "engines": { "node": ">=0.10.0" } diff --git a/package.json b/package.json index 9e24776bbf42..b3a47c387b6c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "new.expensify", - "version": "9.0.55-0", + "version": "9.0.57-3", "author": "Expensify, Inc.", "homepage": "https://new.expensify.com", "description": "New Expensify is the next generation of Expensify: a reimagination of payments based atop a foundation of chat.", @@ -33,6 +33,7 @@ "ios-build": "bundle exec fastlane ios build_unsigned", "android-build": "bundle exec fastlane android build_local", "test": "TZ=utc NODE_OPTIONS=--experimental-vm-modules jest", + "perf-test": "NODE_OPTIONS=--experimental-vm-modules npx reassure", "typecheck": "NODE_OPTIONS=--max_old_space_size=8192 tsc", "lint": "NODE_OPTIONS=--max_old_space_size=8192 eslint . --max-warnings=0 --cache --cache-location=node_modules/.cache/eslint", "lint-changed": "NODE_OPTIONS=--max_old_space_size=8192 eslint --max-warnings=0 --config ./.eslintrc.changed.js $(git diff --diff-filter=AM --name-only origin/main HEAD -- \"*.ts\" \"*.tsx\")", @@ -67,7 +68,7 @@ }, "dependencies": { "@dotlottie/react-player": "^1.6.3", - "@expensify/react-native-live-markdown": "0.1.176", + "@expensify/react-native-live-markdown": "0.1.179", "@expo/metro-runtime": "~3.2.3", "@firebase/app": "^0.10.10", "@firebase/performance": "^0.6.8", @@ -107,7 +108,7 @@ "date-fns-tz": "^3.2.0", "dom-serializer": "^0.2.2", "domhandler": "^4.3.0", - "expensify-common": "2.0.100", + "expensify-common": "2.0.101", "expo": "51.0.31", "expo-av": "14.0.7", "expo-image": "1.12.15", @@ -133,11 +134,10 @@ "react-map-gl": "^7.1.3", "react-native": "0.75.2", "react-native-android-location-enabler": "^2.0.1", - "react-native-app-logs": "git+https://github.com/margelo/react-native-app-logs#7e9c311bffdc6a9eeb69d90d30ead47e01c3552a", + "react-native-app-logs": "0.3.1", "react-native-blob-util": "0.19.4", "react-native-collapsible": "^1.6.2", "react-native-config": "1.5.3", - "react-native-dev-menu": "^4.1.1", "react-native-device-info": "10.3.1", "react-native-document-picker": "^9.3.1", "react-native-draggable-flatlist": "^4.0.1", @@ -213,6 +213,7 @@ "@perf-profiler/profiler": "^0.10.10", "@perf-profiler/reporter": "^0.9.0", "@perf-profiler/types": "^0.8.0", + "@pmmmwh/react-refresh-webpack-plugin": "^0.5.15", "@react-native-community/eslint-config": "3.2.0", "@react-native/babel-preset": "0.75.2", "@react-native/metro-config": "0.75.2", @@ -275,7 +276,7 @@ "electron-builder": "25.0.0", "eslint": "^8.57.0", "eslint-config-airbnb-typescript": "^18.0.0", - "eslint-config-expensify": "^2.0.66", + "eslint-config-expensify": "^2.0.73", "eslint-config-prettier": "^9.1.0", "eslint-plugin-deprecation": "^3.0.0", "eslint-plugin-jest": "^28.6.0", @@ -308,6 +309,7 @@ "react-compiler-runtime": "^19.0.0-beta-8a03594-20241020", "react-is": "^18.3.1", "react-native-clean-project": "^4.0.0-alpha4.0", + "react-refresh": "^0.14.2", "react-test-renderer": "18.3.1", "reassure": "^1.0.0-rc.4", "semver": "7.5.2", @@ -367,13 +369,11 @@ }, "electronmon": { "patterns": [ - "!node_modules", - "!node_modules/**/*", - "!**/*.map", + "!src/**", "!ios/**", "!android/**", - "*.test.*", - "*.spec.*" + "!tests/**", + "*.test.*" ] }, "engines": { diff --git a/patches/react-native+0.75.2+019+Android-onHostResume-resume-frame-callback.patch b/patches/react-native+0.75.2+019+Android-onHostResume-resume-frame-callback.patch new file mode 100644 index 000000000000..3bea9012e9d5 --- /dev/null +++ b/patches/react-native+0.75.2+019+Android-onHostResume-resume-frame-callback.patch @@ -0,0 +1,23 @@ +diff --git a/node_modules/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/events/FabricEventDispatcher.java b/node_modules/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/events/FabricEventDispatcher.java +index ce8520e..e2b22b0 100644 +--- a/node_modules/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/events/FabricEventDispatcher.java ++++ b/node_modules/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/events/FabricEventDispatcher.java +@@ -122,6 +122,7 @@ public class FabricEventDispatcher implements EventDispatcher, LifecycleEventLis + @Override + public void onHostResume() { + maybePostFrameCallbackFromNonUI(); ++ mCurrentFrameCallback.resume(); + } + + @Override +@@ -190,6 +191,10 @@ public class FabricEventDispatcher implements EventDispatcher, LifecycleEventLis + mShouldStop = true; + } + ++ public void resume() { ++ mShouldStop = false; ++ } ++ + public void maybePost() { + if (!mIsPosted) { + mIsPosted = true; diff --git a/src/App.tsx b/src/App.tsx index 177cc00c7dee..643e2146e501 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -50,6 +50,8 @@ LogBox.ignoreLogs([ // the timer is lost. Currently Expensify is using a 30 minutes interval to refresh personal details. // More details here: https://git.io/JJYeb 'Setting a timer for a long period of time', + // We are not using expo-const, so ignore the warning. + 'No native ExponentConstants module found', ]); const fill = {flex: 1}; diff --git a/src/CONFIG.ts b/src/CONFIG.ts index d82a261c2ec6..8a30c8bf57c2 100644 --- a/src/CONFIG.ts +++ b/src/CONFIG.ts @@ -97,9 +97,9 @@ export default { }, GCP_GEOLOCATION_API_KEY: googleGeolocationAPIKey, FIREBASE_WEB_CONFIG: { - apiKey: get(Config, 'FB_API_KEY', 'AIzaSyDxzigVLZl4G8MP7jACQ0qpmADMzmrrON0'), - appId: get(Config, 'FB_APP_ID', '1:921154746561:web:7b8213357d07d6e4027c40'), - projectId: get(Config, 'FB_PROJECT_ID', 'expensify-chat'), + apiKey: get(Config, 'FB_API_KEY', 'AIzaSyBrLKgCuo6Vem6Xi5RPokdumssW8HaWBow'), + appId: get(Config, 'FB_APP_ID', '1:1008697809946:web:ca25268d2645fc285445a3'), + projectId: get(Config, 'FB_PROJECT_ID', 'expensify-mobile-app'), }, // to read more about StrictMode see: contributingGuides/STRICT_MODE.md USE_REACT_STRICT_MODE_IN_DEV: false, diff --git a/src/CONST.ts b/src/CONST.ts index 8e240ac43ca1..fbe12d1fdfb2 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -77,6 +77,12 @@ const onboardingChoices = { ...backendOnboardingChoices, } as const; +const combinedTrackSubmitOnboardingChoices = { + PERSONAL_SPEND: selectableOnboardingChoices.PERSONAL_SPEND, + EMPLOYER: selectableOnboardingChoices.EMPLOYER, + SUBMIT: backendOnboardingChoices.SUBMIT, +} as const; + const signupQualifiers = { INDIVIDUAL: 'individual', VSB: 'vsb', @@ -127,6 +133,95 @@ const onboardingEmployerOrSubmitMessage: OnboardingMessageType = { ], }; +const combinedTrackSubmitOnboardingEmployerOrSubmitMessage: OnboardingMessageType = { + ...onboardingEmployerOrSubmitMessage, + tasks: [ + { + type: 'submitExpense', + autoCompleted: false, + title: 'Submit an expense', + description: + '*Submit an expense* by entering an amount or scanning a receipt.\n' + + '\n' + + 'Here’s how to submit an expense:\n' + + '\n' + + '1. Click the green *+* button.\n' + + '2. Choose *Create expense*.\n' + + '3. Enter an amount or scan a receipt.\n' + + '4. Add your reimburser to the request.\n' + + '5. Click *Submit*.\n' + + '\n' + + 'And you’re done! Now wait for that sweet “Cha-ching!” when it’s complete.', + }, + { + type: 'addBankAccount', + autoCompleted: false, + title: 'Add personal bank account', + description: + 'You’ll need to add your personal bank account to get paid back. Don’t worry, it’s easy!\n' + + '\n' + + 'Here’s how to set up your bank account:\n' + + '\n' + + '1. Click your profile picture.\n' + + '2. Click *Wallet* > *Bank accounts* > *+ Add bank account*.\n' + + '3. Connect your bank account.\n' + + '\n' + + 'Once that’s done, you can request money from anyone and get paid back right into your personal bank account.', + }, + ], +}; + +const onboardingPersonalSpendMessage: OnboardingMessageType = { + message: 'Here’s how to track your spend in a few clicks.', + video: { + url: `${CLOUDFRONT_URL}/videos/guided-setup-track-personal-v2.mp4`, + thumbnailUrl: `${CLOUDFRONT_URL}/images/guided-setup-track-personal.jpg`, + duration: 55, + width: 1280, + height: 960, + }, + tasks: [ + { + type: 'trackExpense', + autoCompleted: false, + title: 'Track an expense', + description: + '*Track an expense* in any currency, whether you have a receipt or not.\n' + + '\n' + + 'Here’s how to track an expense:\n' + + '\n' + + '1. Click the green *+* button.\n' + + '2. Choose *Track expense*.\n' + + '3. Enter an amount or scan a receipt.\n' + + '4. Click *Track*.\n' + + '\n' + + 'And you’re done! Yep, it’s that easy.', + }, + ], +}; +const combinedTrackSubmitOnboardingPersonalSpendMessage: OnboardingMessageType = { + ...onboardingPersonalSpendMessage, + tasks: [ + { + type: 'trackExpense', + autoCompleted: false, + title: 'Track an expense', + description: + '*Track an expense* in any currency, whether you have a receipt or not.\n' + + '\n' + + 'Here’s how to track an expense:\n' + + '\n' + + '1. Click the green *+* button.\n' + + '2. Choose *Create expense*.\n' + + '3. Enter an amount or scan a receipt.\n' + + '4. Click "Just track it (don\'t submit it)".\n' + + '5. Click *Track*.\n' + + '\n' + + 'And you’re done! Yep, it’s that easy.', + }, + ], +}; + type OnboardingPurposeType = ValueOf; type OnboardingCompanySizeType = ValueOf; @@ -152,8 +247,25 @@ type OnboardingInviteType = ValueOf; type OnboardingTaskType = { type: string; autoCompleted: boolean; - title: string; - description: string | ((params: Partial<{adminsRoomLink: string; workspaceCategoriesLink: string; workspaceMoreFeaturesLink: string; workspaceMembersLink: string}>) => string); + title: + | string + | (( + params: Partial<{ + integrationName: string; + }>, + ) => string); + description: + | string + | (( + params: Partial<{ + adminsRoomLink: string; + workspaceCategoriesLink: string; + workspaceMoreFeaturesLink: string; + workspaceMembersLink: string; + integrationName: string; + workspaceAccountingLink: string; + }>, + ) => string); }; type OnboardingMessageType = { @@ -475,6 +587,9 @@ const CONST = { }, }, NON_USD_BANK_ACCOUNT: { + ALLOWED_FILE_TYPES: ['pdf', 'jpg', 'jpeg', 'png'], + FILE_LIMIT: 10, + TOTAL_FILES_SIZE_LIMIT: 5242880, STEP: { COUNTRY: 'CountryStep', BANK_INFO: 'BankInfoStep', @@ -505,10 +620,9 @@ const CONST = { COMPANY_CARD_FEEDS: 'companyCardFeeds', DIRECT_FEEDS: 'directFeeds', NETSUITE_USA_TAX: 'netsuiteUsaTax', - NEW_DOT_COPILOT: 'newDotCopilot', - WORKSPACE_RULES: 'workspaceRules', COMBINED_TRACK_SUBMIT: 'combinedTrackSubmit', CATEGORY_AND_TAG_APPROVERS: 'categoryAndTagApprovers', + PER_DIEM: 'newDotPerDiem', }, BUTTON_STATES: { DEFAULT: 'default', @@ -541,6 +655,7 @@ const CONST = { ANDROID: 'android', WEB: 'web', DESKTOP: 'desktop', + MOBILEWEB: 'mobileweb', }, PLATFORM_SPECIFIC_KEYS: { CTRL: { @@ -763,8 +878,10 @@ const CONST = { // Use Environment.getEnvironmentURL to get the complete URL with port number DEV_NEW_EXPENSIFY_URL: 'https://dev.new.expensify.com:', NAVATTIC: { - ADMIN_TOUR: 'https://expensify.navattic.com/kh204a7', - EMPLOYEE_TOUR: 'https://expensify.navattic.com/35609gb', + ADMIN_TOUR_PRODUCTION: 'https://expensify.navattic.com/kh204a7', + ADMIN_TOUR_STAGING: 'https://expensify.navattic.com/3i300k18', + EMPLOYEE_TOUR_PRODUCTION: 'https://expensify.navattic.com/35609gb', + EMPLOYEE_TOUR_STAGING: 'https://expensify.navattic.com/cf15002s', }, OLDDOT_URLS: { @@ -1098,6 +1215,7 @@ const CONST = { MODAL_TYPE: { CONFIRM: 'confirm', CENTERED: 'centered', + CENTERED_SWIPABLE_TO_RIGHT: 'centered_swipable_to_right', CENTERED_UNSWIPEABLE: 'centered_unswipeable', CENTERED_SMALL: 'centered_small', BOTTOM_DOCKED: 'bottom_docked', @@ -1178,7 +1296,13 @@ const CONST = { PENDING: 'Pending', POSTED: 'Posted', }, + STATE: { + CURRENT: 'current', + DRAFT: 'draft', + BACKUP: 'backup', + }, }, + MCC_GROUPS: { AIRLINES: 'Airlines', COMMUTER: 'Commuter', @@ -2576,6 +2700,11 @@ const CONST = { INDIVIDUAL: 'individual', NONE: 'none', }, + VERIFICATION_STATE: { + LOADING: 'loading', + VERIFIED: 'verified', + ON_WAITLIST: 'onWaitlist', + }, STATE: { STATE_NOT_ISSUED: 2, OPEN: 3, @@ -2590,6 +2719,7 @@ const CONST = { MONTHLY: 'monthly', FIXED: 'fixed', }, + LIMIT_VALUE: 21474836, STEP_NAMES: ['1', '2', '3', '4', '5', '6'], STEP: { ASSIGNEE: 'Assignee', @@ -2607,7 +2737,6 @@ const CONST = { DAILY: 'daily', MONTHLY: 'monthly', }, - CARD_TITLE_INPUT_LIMIT: 255, MANAGE_EXPENSIFY_CARDS_ARTICLE_LINK: 'https://help.expensify.com/articles/new-expensify/expensify-card/Manage-Expensify-Cards', }, COMPANY_CARDS: { @@ -2837,6 +2966,7 @@ const CONST = { SHORT_MENTION: new RegExp(`@[\\w\\-\\+\\'#@]+(?:\\.[\\w\\-\\'\\+]+)*(?![^\`]*\`)`, 'gim'), REPORT_ID_FROM_PATH: /\/r\/(\d+)/, DISTANCE_MERCHANT: /^[0-9.]+ \w+ @ (-|-\()?[^0-9.\s]{1,3} ?[0-9.]+\)? \/ \w+$/, + WHITESPACE: /\s+/g, get EXPENSIFY_POLICY_DOMAIN_NAME() { return new RegExp(`${EXPENSIFY_POLICY_DOMAIN}([a-zA-Z0-9]+)\\${EXPENSIFY_POLICY_DOMAIN_EXTENSION}`); @@ -2925,6 +3055,7 @@ const CONST = { // Character Limits FORM_CHARACTER_LIMIT: 50, + STANDARD_LENGTH_LIMIT: 100, LEGAL_NAMES_CHARACTER_LIMIT: 150, LOGIN_CHARACTER_LIMIT: 254, CATEGORY_NAME_LIMIT: 256, @@ -4239,6 +4370,7 @@ const CONST = { // The attribute used in the SelectionScraper.js helper to query all the DOM elements // that should be removed from the copied contents in the getHTMLOfSelection() method SELECTION_SCRAPER_HIDDEN_ELEMENT: 'selection-scrapper-hidden-element', + INNER_BOX_SHADOW_ELEMENT: 'inner-box-shadow-element', MODERATION: { MODERATOR_DECISION_PENDING: 'pending', MODERATOR_DECISION_PENDING_HIDE: 'pendingHide', @@ -4672,6 +4804,7 @@ const CONST = { ONBOARDING_INTRODUCTION: 'Let’s get you set up 🔧', ONBOARDING_CHOICES: {...onboardingChoices}, SELECTABLE_ONBOARDING_CHOICES: {...selectableOnboardingChoices}, + COMBINED_TRACK_SUBMIT_ONBOARDING_CHOICES: {...combinedTrackSubmitOnboardingChoices}, ONBOARDING_SIGNUP_QUALIFIERS: {...signupQualifiers}, ONBOARDING_INVITE_TYPES: {...onboardingInviteTypes}, ONBOARDING_COMPANY_SIZE: {...onboardingCompanySize}, @@ -4715,7 +4848,13 @@ const CONST = { '\n' + "We'll send a request to each person so they can pay you back. Let me know if you have any questions!", }, - + ONBOARDING_ACCOUNTING_MAPPING: { + quickbooksOnline: 'QuickBooks Online', + xero: 'Xero', + netsuite: 'NetSuite', + intacct: 'Sage Intacct', + quickbooksDesktop: 'QuickBooks Desktop', + }, ONBOARDING_MESSAGES: { [onboardingChoices.EMPLOYER]: onboardingEmployerOrSubmitMessage, [onboardingChoices.SUBMIT]: onboardingEmployerOrSubmitMessage, @@ -4827,36 +4966,27 @@ const CONST = { '\n' + `[Take me to workspace members](${workspaceMembersLink}). That’s it, happy expensing! :)`, }, - ], - }, - [onboardingChoices.PERSONAL_SPEND]: { - message: 'Here’s how to track your spend in a few clicks.', - video: { - url: `${CLOUDFRONT_URL}/videos/guided-setup-track-personal-v2.mp4`, - thumbnailUrl: `${CLOUDFRONT_URL}/images/guided-setup-track-personal.jpg`, - duration: 55, - width: 1280, - height: 960, - }, - tasks: [ { - type: 'trackExpense', + type: 'addAccountingIntegration', autoCompleted: false, - title: 'Track an expense', - description: - '*Track an expense* in any currency, whether you have a receipt or not.\n' + + title: ({integrationName}) => `Connect to ${integrationName}`, + description: ({integrationName, workspaceAccountingLink}) => + `Connect to ${integrationName} for automatic expense coding and syncing that makes month-end close a breeze.\n` + '\n' + - 'Here’s how to track an expense:\n' + + `Here’s how to connect to ${integrationName}:\n` + '\n' + - '1. Click the green *+* button.\n' + - '2. Choose *Track expense*.\n' + - '3. Enter an amount or scan a receipt.\n' + - '4. Click *Track*.\n' + + '1. Click your profile photo.\n' + + '2. Go to Workspaces.\n' + + '3. Select your workspace.\n' + + '4. Click Accounting.\n' + + `5. Find ${integrationName}.\n` + + '6. Click Connect.\n' + '\n' + - 'And you’re done! Yep, it’s that easy.', + `[Take me to Accounting!](${workspaceAccountingLink})`, }, ], }, + [onboardingChoices.PERSONAL_SPEND]: onboardingPersonalSpendMessage, [onboardingChoices.CHAT_SPLIT]: { message: 'Splitting bills with friends is as easy as sending a message. Here’s how.', video: { @@ -4925,6 +5055,12 @@ const CONST = { }, } satisfies Record, + COMBINED_TRACK_SUBMIT_ONBOARDING_MESSAGES: { + [combinedTrackSubmitOnboardingChoices.PERSONAL_SPEND]: combinedTrackSubmitOnboardingPersonalSpendMessage, + [combinedTrackSubmitOnboardingChoices.EMPLOYER]: combinedTrackSubmitOnboardingEmployerOrSubmitMessage, + [combinedTrackSubmitOnboardingChoices.SUBMIT]: combinedTrackSubmitOnboardingEmployerOrSubmitMessage, + } satisfies Record, OnboardingMessageType>, + REPORT_FIELD_TITLE_FIELD_ID: 'text_title', MOBILE_PAGINATION_SIZE: 15, @@ -5799,6 +5935,11 @@ const CONST = { IN: 'in', }, EMPTY_VALUE: 'none', + SEARCH_ROUTER_ITEM_TYPE: { + CONTEXTUAL_SUGGESTION: 'contextualSuggestion', + AUTOCOMPLETE_SUGGESTION: 'autocompleteSuggestion', + SEARCH: 'searchItem', + }, }, REFERRER: { diff --git a/src/ONYXKEYS.ts b/src/ONYXKEYS.ts index 427e05052ae3..7d3d0edef36e 100755 --- a/src/ONYXKEYS.ts +++ b/src/ONYXKEYS.ts @@ -1,6 +1,7 @@ import type {ValueOf} from 'type-fest'; import type CONST from './CONST'; import type {OnboardingCompanySizeType, OnboardingPurposeType} from './CONST'; +import type Platform from './libs/getPlatform/types'; import type * as FormTypes from './types/form'; import type * as OnyxTypes from './types/onyx'; import type {Attendee} from './types/onyx/IOU'; @@ -122,6 +123,9 @@ const ONYXKEYS = { /** This NVP contains data associated with HybridApp */ NVP_TRYNEWDOT: 'nvp_tryNewDot', + /** Contains the platforms for which the user muted the sounds */ + NVP_MUTED_PLATFORMS: 'nvp_mutedPlatforms', + /** Contains the user preference for the LHN priority mode */ NVP_PRIORITY_MODE: 'nvp_priorityMode', @@ -527,6 +531,9 @@ const ONYXKEYS = { /** Currently displaying feed */ LAST_SELECTED_FEED: 'lastSelectedFeed_', + + /** Whether the bank account chosen for Expensify Card in on verification waitlist */ + NVP_EXPENSIFY_ON_CARD_WAITLIST: 'nvp_expensify_onCardWaitlist_', }, /** List of Form ids */ @@ -857,6 +864,7 @@ type OnyxCollectionValuesMapping = { [ONYXKEYS.COLLECTION.EXPENSIFY_CARD_CONTINUOUS_RECONCILIATION_CONNECTION]: OnyxTypes.PolicyConnectionName; [ONYXKEYS.COLLECTION.EXPENSIFY_CARD_USE_CONTINUOUS_RECONCILIATION]: boolean; [ONYXKEYS.COLLECTION.LAST_SELECTED_FEED]: OnyxTypes.CompanyCardFeed; + [ONYXKEYS.COLLECTION.NVP_EXPENSIFY_ON_CARD_WAITLIST]: OnyxTypes.CardOnWaitlist; }; type OnyxValuesMapping = { @@ -903,6 +911,7 @@ type OnyxValuesMapping = { [ONYXKEYS.USER_METADATA]: OnyxTypes.UserMetadata; [ONYXKEYS.STASHED_SESSION]: OnyxTypes.Session; [ONYXKEYS.BETAS]: OnyxTypes.Beta[]; + [ONYXKEYS.NVP_MUTED_PLATFORMS]: Partial>; [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..9a90de17595d 100644 --- a/src/components/AccountSwitcher.tsx +++ b/src/components/AccountSwitcher.tsx @@ -5,7 +5,6 @@ import {useOnyx} from 'react-native-onyx'; import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails'; import useLocalize from '@hooks/useLocalize'; import useNetwork from '@hooks/useNetwork'; -import usePermissions from '@hooks/usePermissions'; import useResponsiveLayout from '@hooks/useResponsiveLayout'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; @@ -34,7 +33,6 @@ function AccountSwitcher() { const theme = useTheme(); const {translate} = useLocalize(); const {isOffline} = useNetwork(); - const {canUseNewDotCopilot} = usePermissions(); const {shouldUseNarrowLayout} = useResponsiveLayout(); const [account] = useOnyx(ONYXKEYS.ACCOUNT); const [session] = useOnyx(ONYXKEYS.SESSION); @@ -47,7 +45,7 @@ function AccountSwitcher() { const delegators = account?.delegatedAccess?.delegators ?? []; const isActingAsDelegate = !!account?.delegatedAccess?.delegate ?? false; - const canSwitchAccounts = canUseNewDotCopilot && (delegators.length > 0 || isActingAsDelegate); + const canSwitchAccounts = delegators.length > 0 || isActingAsDelegate; const createBaseMenuItem = ( personalDetails: PersonalDetails | undefined, @@ -87,7 +85,7 @@ function AccountSwitcher() { } const delegatePersonalDetails = PersonalDetailsUtils.getPersonalDetailByEmail(delegateEmail); - const error = ErrorUtils.getLatestErrorField(account?.delegatedAccess, 'connect'); + const error = ErrorUtils.getLatestError(account?.delegatedAccess?.errorFields?.disconnect); return [ createBaseMenuItem(delegatePersonalDetails, error, { @@ -105,8 +103,9 @@ function AccountSwitcher() { const delegatorMenuItems: PopoverMenuItem[] = delegators .filter(({email}) => email !== currentUserPersonalDetails.login) - .map(({email, role, errorFields}) => { - const error = ErrorUtils.getLatestErrorField({errorFields}, 'connect'); + .map(({email, role}) => { + const errorFields = account?.delegatedAccess?.errorFields ?? {}; + const error = ErrorUtils.getLatestError(errorFields?.connect?.[email]); const personalDetails = PersonalDetailsUtils.getPersonalDetailByEmail(email); return createBaseMenuItem(personalDetails, error, { badgeText: translate('delegate.role', {role}), @@ -152,7 +151,7 @@ function AccountSwitcher() { > {currentUserPersonalDetails?.displayName} - {canSwitchAccounts && ( + {!!canSwitchAccounts && ( - {canSwitchAccounts && ( + {!!canSwitchAccounts && ( { diff --git a/src/components/AmountForm.tsx b/src/components/AmountForm.tsx index 4848577bdea0..a230dfa1af8d 100644 --- a/src/components/AmountForm.tsx +++ b/src/components/AmountForm.tsx @@ -1,6 +1,6 @@ import type {ForwardedRef} from 'react'; import React, {forwardRef, useCallback, useEffect, useMemo, useRef, useState} from 'react'; -import type {NativeSyntheticEvent, TextInputSelectionChangeEventData} from 'react-native'; +import type {NativeSyntheticEvent} from 'react-native'; import {View} from 'react-native'; import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; @@ -16,7 +16,7 @@ import TextInput from './TextInput'; import isTextInputFocused from './TextInput/BaseTextInput/isTextInputFocused'; import type {BaseTextInputProps, BaseTextInputRef} from './TextInput/BaseTextInput/types'; import TextInputWithCurrencySymbol from './TextInputWithCurrencySymbol'; -import type TextInputWithCurrencySymbolProps from './TextInputWithCurrencySymbol/types'; +import type BaseTextInputWithCurrencySymbolProps from './TextInputWithCurrencySymbol/types'; type AmountFormProps = { /** Amount supplied by the FormProvider */ @@ -51,7 +51,7 @@ type AmountFormProps = { /** Number of decimals to display */ fixedDecimals?: number; -} & Pick & +} & Pick & Pick; /** @@ -290,11 +290,11 @@ function AmountForm( }} selectedCurrencyCode={currency} selection={selection} - onSelectionChange={(e: NativeSyntheticEvent) => { + onSelectionChange={(start, end) => { if (!shouldUpdateSelection) { return; } - setSelection(e.nativeEvent.selection); + setSelection({start, end}); }} onKeyPress={textInputKeyPress} isCurrencyPressable={isCurrencyPressable} diff --git a/src/components/AttachmentPicker/index.native.tsx b/src/components/AttachmentPicker/index.native.tsx index d3a51c7fc0f0..5305155ae495 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 [isVisible, setIsVisible] = useState(false); + const StyleUtils = useStyleUtils(); + const theme = useTheme(); - 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