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/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/contributingGuides/BUGZERO_CHECKLIST.md b/contributingGuides/BUGZERO_CHECKLIST.md new file mode 100644 index 000000000000..00075620641c --- /dev/null +++ b/contributingGuides/BUGZERO_CHECKLIST.md @@ -0,0 +1,62 @@ +# BugZero Checklist: + +- [ ] **[Contributor]** Classify the bug: + +
+Bug classification + + +Source of bug: + - [ ] 1a. Result of the original design (eg. a case wasn't considered) + - [ ] 1b. Mistake during implementation + - [ ] 1c. Backend bug + - [ ] 1z. Other: + +Where bug was reported: + - [ ] 2a. Reported on production + - [ ] 2b. Reported on staging (deploy blocker) + - [ ] 2c. Reported on a PR + - [ ] 2z. Other: + +Who reported the bug: + - [ ] 3a. Expensify user + - [ ] 3b. Expensify employee + - [ ] 3c. Contributor + - [ ] 3d. QA + - [ ] 3z. Other: + +
+ +- [ ] **[Contributor]** The offending PR has been commented on, pointing out the bug it caused and why, so the author and reviewers can learn from the mistake. + + Link to comment: + +- [ ] **[Contributor]** If the regression was CRITICAL (e.g. interrupts a core flow) A discussion in [#expensify-open-source](https://app.slack.com/client/E047TPA624F/C01GTK53T8Q) has been started about whether any other steps should be taken (e.g. updating the PR review checklist) in order to catch this type of bug sooner. + + Link to discussion: + +- [ ] **[Contributor]** If it was decided to create a regression test for the bug, please propose the [regression test](https://github.com/Expensify/App/blob/main/contributingGuides/REGRESSION_TEST_BEST_PRACTICES.md) steps using the template below to ensure the same bug will not reach production again. + +
+Regression Test Proposal Template + + +- [ ] **[BugZero Assignee]** Create a GH issue for creating/updating the regression test once above steps have been agreed upon. + + Link to issue: + +## Regression Test Proposal +### Precondition: + + +- + +### Test: + + +1. + +Do we agree 👍 or 👎 + + +
diff --git a/tests/perf-test/README.md b/contributingGuides/REASSURE_PERFORMANCE_TEST.md similarity index 90% rename from tests/perf-test/README.md rename to contributingGuides/REASSURE_PERFORMANCE_TEST.md index 2b66f7c147f3..0de450b78875 100644 --- a/tests/perf-test/README.md +++ b/contributingGuides/REASSURE_PERFORMANCE_TEST.md @@ -7,8 +7,11 @@ We use Reassure for monitoring performance regression. It helps us check if our - Reassure builds on the existing React Testing Library setup and adds a performance measurement functionality. It's intended to be used on local machine and on a remote server as part of your continuous integration setup. - To make sure the results are reliable and consistent, Reassure runs tests twice – once for the current branch and once for the base branch. -## Performance Testing Strategy (`measurePerformance`) +## Performance Testing Strategy (`measureRenders`) +- Before adding new tests, check if the proposed scenario or component is already covered in existing tests. Duplicate tests can slow down the CI suite, making it harder to spot meaningful regressions. +- Test only scenarios that cover new or unique interactions. Avoid testing repetitive user actions that could be captured within a single, comprehensive scenario. +- Where applicable, use utility functions and helper methods to consolidate common actions (e.g., data mocking, scenario setup) across tests. This reduces redundancy and allows tests to be more focused and reusable. - The primary focus is on testing business cases rather than small, reusable parts that typically don't introduce regressions, although some tests in that area are still necessary. - To achieve this goal, it's recommended to stay relatively high up in the React tree, targeting whole screens to recreate real-life scenarios that users may encounter. - For example, consider scenarios where an additional `useMemo` call could impact performance negatively. @@ -84,7 +87,7 @@ test('Count increments on press', async () => { await screen.findByText('Count: 2'); }; - await measurePerformance( + await measureRenders( , { scenario, runs: 20 } ); diff --git a/contributingGuides/REVIEWER_CHECKLIST.md b/contributingGuides/REVIEWER_CHECKLIST.md index 4ff1f01b1475..5fc14328f3b4 100644 --- a/contributingGuides/REVIEWER_CHECKLIST.md +++ b/contributingGuides/REVIEWER_CHECKLIST.md @@ -19,7 +19,6 @@ - [ ] If there are any errors in the console that are unrelated to this PR, I either fixed them (preferred) or linked to where I reported them in Slack - [ ] I verified proper code patterns were followed (see [Reviewing the code](https://github.com/Expensify/App/blob/main/contributingGuides/PR_REVIEW_GUIDELINES.md#reviewing-the-code)) - [ ] I verified that any callback methods that were added or modified are named for what the method does and never what callback they handle (i.e. `toggleReport` and not `onIconClick`). - - [ ] I verified that the left part of a conditional rendering a React component is a boolean and NOT a string, e.g. `myBool && `. - [ ] I verified that comments were added to code that is not self explanatory - [ ] I verified that any new or modified comments were clear, correct English, and explained "why" the code was doing something instead of only explaining "what" the code was doing. - [ ] I verified any copy / text shown in the product is localized by adding it to `src/languages/*` files and using the [translation method](https://github.com/Expensify/App/blob/4bd99402cebdf4d7394e0d1f260879ea238197eb/src/components/withLocalize.js#L60) diff --git a/docs/articles/expensify-classic/bank-accounts-and-payments/payments/Pay-an-Invoice.md b/docs/articles/expensify-classic/bank-accounts-and-payments/payments/Pay-an-Invoice.md index 84fafc949527..18020402f7de 100644 --- a/docs/articles/expensify-classic/bank-accounts-and-payments/payments/Pay-an-Invoice.md +++ b/docs/articles/expensify-classic/bank-accounts-and-payments/payments/Pay-an-Invoice.md @@ -10,9 +10,15 @@ There are multiple ways to pay Invoices in Expensify. Let’s go over each metho # How to Pay Invoices 1. Sign in to your [Expensify web account](www.expensify.com). -2. Click on the Invoice you’d like to pay to see the details. -3. Click on the **Pay** button. -4. Follow the prompts to pay through one of the following methods. +2. Click on **Home** and find the pending Invoice payment +3. Click **Pay** to be redirected to the Invoice +4. Review the Invoice +5. When you are ready to pay, click the **Pay** button at the top of the Invoice +6. Follow the prompts to pay through one of the following methods. + +![Click Home and Pay on the invoice](https://help.expensify.com/assets/images/ExpensifyHelp_OldDot_PayInvoice_1.png){:width="100%"} + +![Click Pay on Invoice and choose a method of payment](https://help.expensify.com/assets/images/ExpensifyHelp_OldDot_PayInvoice_2.png){:width="100%"} ### ACH bank-to-bank transfer diff --git a/docs/articles/expensify-classic/expenses/Add-Invoices-in-Bulk.md b/docs/articles/expensify-classic/expenses/Add-Invoices-in-Bulk.md index 6257c1e6d84d..3e9b6c0397db 100644 --- a/docs/articles/expensify-classic/expenses/Add-Invoices-in-Bulk.md +++ b/docs/articles/expensify-classic/expenses/Add-Invoices-in-Bulk.md @@ -14,6 +14,13 @@ Expensify offers importing multiple invoices (bulk import) via CSV to save you f 5. Add the invoice details following the formatting rules (see below **CSV formatting guide** section) 6. Click **Upload CSV** +![Click Reports, New Reports, choose Bulk Import Invoices](https://help.expensify.com/assets/images/invoice-bulk-01.png){:width="100%"} + +![Download Sample CSV](https://help.expensify.com/assets/images/invoice-bulk-02.png){:width="100%"} + +![Format CSV following our guidelines](https://help.expensify.com/assets/images/invoice-bulk-03.png){:width="100%"} + + ## CSV formatting guide - Send to: recipient's email address (ex: john.smith@companydomain.com) @@ -27,10 +34,15 @@ Expensify offers importing multiple invoices (bulk import) via CSV to save you f ## After the Invoices are uploaded - After you click **Upload**, the invoices will automatically be created and viewable on the **Reports** page. +- Set the **Reports page** filter to Invoices to narrow down your search. - The **Send To** contact will get an email notifying them of the invoice you sent. - You can manually edit the invoice details. - You can manually upload a PDF of the invoice to the report. +![Search for Invoices on Reports page](https://help.expensify.com/assets/images/invoice-bulk-04.png){:width="100%"} + +![Invoices will indicate next steps at the top of each report](https://help.expensify.com/assets/images/invoice-bulk-05.png){:width="100%"} + {% include faq-begin.md %} ## Are there any fees associated with Invoices in Expensify? diff --git a/docs/articles/expensify-classic/expenses/Add-an-expense.md b/docs/articles/expensify-classic/expenses/Add-an-expense.md index 461748c6af9e..92a96e989013 100644 --- a/docs/articles/expensify-classic/expenses/Add-an-expense.md +++ b/docs/articles/expensify-classic/expenses/Add-an-expense.md @@ -88,7 +88,7 @@ You can also email receipts to SmartScan by sending them to receipts@expensify.c If you are an employee under a company workspace, you may not see all of the different expense type options depending on your company’s workspace settings. {% include end-info.html %} -# FAQs +{% include faq-begin.md %} **What’s the difference between a reimbursable and non-reimbursable expense?** @@ -99,4 +99,5 @@ If you are an employee under a company workspace, you may not see all of the dif If you are an employee under a company workspace, your expenses may automatically be configured as reimbursable or non-reimbursable depending on the details that are entered. If an expense is incorrectly labeled, you must reach out to an admin to have it corrected. {% include end-info.html %} +{% include faq-end.md %} diff --git a/docs/articles/expensify-classic/expenses/Send-and-Receive-Payment-for-Invoices.md b/docs/articles/expensify-classic/expenses/Send-and-Receive-Payment-for-Invoices.md index c2ebb64b0af6..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 66955822aab7..42f887a827d1 100644 --- a/fastlane/Appfile +++ b/fastlane/Appfile @@ -1,5 +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 +# 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 eed84acdc916..74bcff5bf320 100644 --- a/fastlane/Fastfile +++ b/fastlane/Fastfile @@ -68,6 +68,23 @@ platform :android do setGradleOutputsInEnv() end + desc "Generate a production HybridApp AAB" + lane :build_hybrid do + ENV["ENVFILE"]="../.env.production.hybridapp" + gradle( + project_dir: '../Android', + task: "bundleRelease", + flags: "--refresh-dependencies", + properties: { + "android.injected.signing.store.file" => './upload-key.keystore', + "android.injected.signing.store.password" => ENV["ANDROID_UPLOAD_KEYSTORE_PASSWORD"], + "android.injected.signing.key.alias" => ENV["ANDROID_UPLOAD_KEYSTORE_ALIAS"], + "android.injected.signing.key.password" => ENV["ANDROID_UPLOAD_KEY_PASSWORD"], + } + ) + setGradleOutputsInEnv() + end + desc "Generate a new local APK" lane :build_local do ENV["ENVFILE"]=".env.production" @@ -80,6 +97,18 @@ platform :android do setGradleOutputsInEnv() end + desc "Generate a new local HybridApp APK" + lane :build_local_hybrid do + ENV["ENVFILE"]=".env.production" + gradle( + project_dir: '../Android', + task: 'assemble', + flavor: 'Production', + build_type: 'Release', + ) + setGradleOutputsInEnv() + end + desc "Generate a new local APK for e2e testing" lane :build_e2e do ENV["ENVFILE"]="tests/e2e/.env.e2e" @@ -151,6 +180,38 @@ platform :android do ) end + desc "Upload HybridApp to Google Play for internal testing" + lane :upload_google_play_internal_hybrid do + # Google is very unreliable, so we retry a few times + ENV["SUPPLY_UPLOAD_MAX_RETRIES"]="5" + upload_to_play_store( + package_name: "org.me.mobiexpensifyg", + json_key: './android-fastlane-json-key.json', + aab: ENV[KEY_GRADLE_AAB_PATH], + 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" lane :upload_google_play_production do # Google is very unreliable, so we retry a few times @@ -228,6 +289,37 @@ platform :ios do setIOSBuildOutputsInEnv() end + desc "Build an iOS HybridApp production build" + lane :build_hybrid do + ENV["ENVFILE"]="../.env.production.hybridapp" + + setupIOSSigningCertificate() + + install_provisioning_profile( + path: "./OldApp_AppStore.mobileprovision" + ) + + install_provisioning_profile( + path: "./OldApp_AppStore_Share_Extension.mobileprovision" + ) + + build_app( + workspace: "../iOS/Expensify.xcworkspace", + scheme: "Expensify", + output_name: "Expensify.ipa", + export_method: "app-store", + export_options: { + manageAppVersionAndBuildNumber: false, + provisioningProfiles: { + "com.expensify.expensifylite" => "(OldApp) AppStore", + "com.expensify.expensifylite.SmartScanExtension" => "(OldApp) AppStore: Share Extension" + } + } + ) + + setIOSBuildOutputsInEnv() + end + desc "Build an unsigned iOS production build" lane :build_unsigned do ENV["ENVFILE"]=".env.production" @@ -238,6 +330,16 @@ platform :ios do setIOSBuildOutputsInEnv() end + desc "Build an unsigned iOS HybridApp production build" + lane :build_unsigned_hybrid do + ENV["ENVFILE"]="../Mobile-Expensify/.env.production.hybridapp" + build_app( + workspace: "../Mobile-Expensify/iOS/Expensify.xcworkspace", + scheme: "Expensify" + ) + setIOSBuildOutputsInEnv() + end + desc "Build AdHoc app for testing" lane :build_adhoc do ENV["ENVFILE"]=".env.adhoc" @@ -286,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, @@ -316,9 +419,45 @@ platform :ios do ) end - desc "Submit app to App Store Review" + 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, + changelog: "Thank you for beta testing New Expensify, this version includes bug fixes and improvements.", + groups: ["Applause", "Beta Testers", "Expensify Employees"], + demo_account_required: true, + beta_app_review_info: { + contact_email: ENV["APPLE_CONTACT_EMAIL"], + contact_first_name: "Andrew", + contact_last_name: "Gable", + contact_phone: ENV["APPLE_CONTACT_PHONE"], + demo_account_name: ENV["APPLE_DEMO_EMAIL"], + demo_account_password: ENV["APPLE_DEMO_PASSWORD"], + notes: "1. In the Expensify app, enter the email 'appletest.expensify@proton.me'. This will trigger a sign-in link to be sent to 'appletest.expensify@proton.me' + 2. Navigate to https://account.proton.me/login, log into Proton Mail using 'appletest.expensify@proton.me' as email and the password associated with 'appletest.expensify@proton.me', provided above + 3. Once logged into Proton Mail, navigate to your inbox and locate the email triggered in step 1. The email subject should be 'Your magic sign-in link for Expensify' + 4. Open the email and copy the 6-digit sign-in code provided within + 5. Return to the Expensify app and enter the copied 6-digit code in the designated login field" + } + ) + + puts "dsym path: #{ENV[KEY_DSYM_PATH]}" + upload_symbols_to_crashlytics( + app_id: "1:1008697809946:ios:3ffad71f664f2886", + dsym_path: ENV[KEY_DSYM_PATH], + gsp_path: "./ios/GoogleService-Info.plist", + # Assuming we are running this from the react-native submodule directory for HybridApp + binary_path: "../iOS/Pods/FirebaseCrashlytics/upload-symbols" + ) + end + + 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.54 + 9.0.56 CFBundleSignature ???? CFBundleURLTypes @@ -40,7 +40,7 @@ CFBundleVersion - 9.0.54.1 + 9.0.56.7 FullStory OrgId diff --git a/ios/NewExpensifyTests/Info.plist b/ios/NewExpensifyTests/Info.plist index cb867d7af0b5..f95e27e3a725 100644 --- a/ios/NewExpensifyTests/Info.plist +++ b/ios/NewExpensifyTests/Info.plist @@ -15,10 +15,10 @@ CFBundlePackageType BNDL CFBundleShortVersionString - 9.0.54 + 9.0.56 CFBundleSignature ???? CFBundleVersion - 9.0.54.1 + 9.0.56.7 diff --git a/ios/NotificationServiceExtension/Info.plist b/ios/NotificationServiceExtension/Info.plist index c7c9879bb2ab..9ed1578badb6 100644 --- a/ios/NotificationServiceExtension/Info.plist +++ b/ios/NotificationServiceExtension/Info.plist @@ -11,9 +11,9 @@ CFBundleName $(PRODUCT_NAME) CFBundleShortVersionString - 9.0.54 + 9.0.56 CFBundleVersion - 9.0.54.1 + 9.0.56.7 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 7202ef76ab66..c3af1507f286 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,17 +1,17 @@ { "name": "new.expensify", - "version": "9.0.54-1", + "version": "9.0.56-7", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "new.expensify", - "version": "9.0.54-1", + "version": "9.0.56-7", "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 f0425a747967..295066f32b9b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "new.expensify", - "version": "9.0.54-1", + "version": "9.0.56-7", "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+018+Add-regex-to-TextInput.patch b/patches/react-native+0.75.2+018+Add-regex-to-TextInput.patch new file mode 100644 index 000000000000..6c511d8cbec1 --- /dev/null +++ b/patches/react-native+0.75.2+018+Add-regex-to-TextInput.patch @@ -0,0 +1,299 @@ +diff --git a/node_modules/react-native/Libraries/Components/TextInput/AndroidTextInputNativeComponent.js b/node_modules/react-native/Libraries/Components/TextInput/AndroidTextInputNativeComponent.js +index 770dfee..73e439b 100644 +--- a/node_modules/react-native/Libraries/Components/TextInput/AndroidTextInputNativeComponent.js ++++ b/node_modules/react-native/Libraries/Components/TextInput/AndroidTextInputNativeComponent.js +@@ -329,6 +329,12 @@ export type NativeProps = $ReadOnly<{| + */ + returnKeyType?: WithDefault, + ++ /** ++ * Restricts the text value to match the specified regular expression. Use this ++ * instead of implementing the logic in JS to avoid flicker. ++ */ ++ regex?: ?string, ++ + /** + * Limits the maximum number of characters that can be entered. Use this + * instead of implementing the logic in JS to avoid flicker. +@@ -699,6 +705,7 @@ export const __INTERNAL_VIEW_CONFIG: PartialViewConfig = { + process: require('../../StyleSheet/processColor').default, + }, + maxLength: true, ++ regex: true, + selectTextOnFocus: true, + textShadowRadius: true, + underlineColorAndroid: { +diff --git a/node_modules/react-native/Libraries/Components/TextInput/RCTTextInputViewConfig.js b/node_modules/react-native/Libraries/Components/TextInput/RCTTextInputViewConfig.js +index dbfe5d5..1f359ba 100644 +--- a/node_modules/react-native/Libraries/Components/TextInput/RCTTextInputViewConfig.js ++++ b/node_modules/react-native/Libraries/Components/TextInput/RCTTextInputViewConfig.js +@@ -151,6 +151,7 @@ const RCTTextInputViewConfig = { + autoFocus: true, + lineBreakStrategyIOS: true, + smartInsertDelete: true, ++ regex: true, + ...ConditionallyIgnoredEventHandlers({ + onClear: true, + onChange: true, +diff --git a/node_modules/react-native/Libraries/Components/TextInput/TextInput.d.ts b/node_modules/react-native/Libraries/Components/TextInput/TextInput.d.ts +index 20501f7..76f30b9 100644 +--- a/node_modules/react-native/Libraries/Components/TextInput/TextInput.d.ts ++++ b/node_modules/react-native/Libraries/Components/TextInput/TextInput.d.ts +@@ -701,6 +701,12 @@ export interface TextInputProps + */ + inputMode?: InputModeOptions | undefined; + ++ /** ++ * Restricts the text value to match the specified regular expression. Use this ++ * instead of implementing the logic in JS to avoid flicker. ++ */ ++ regex?: string | undefined; ++ + /** + * Limits the maximum number of characters that can be entered. + * Use this instead of implementing the logic in JS to avoid flicker. +diff --git a/node_modules/react-native/Libraries/Components/TextInput/TextInput.flow.js b/node_modules/react-native/Libraries/Components/TextInput/TextInput.flow.js +index 2f35731..5bb94bc 100644 +--- a/node_modules/react-native/Libraries/Components/TextInput/TextInput.flow.js ++++ b/node_modules/react-native/Libraries/Components/TextInput/TextInput.flow.js +@@ -697,6 +697,12 @@ export type Props = $ReadOnly<{| + */ + maxFontSizeMultiplier?: ?number, + ++ /** ++ * Restricts the text value to match the specified regular expression. Use this ++ * instead of implementing the logic in JS to avoid flicker. ++ */ ++ regex?: ?string, ++ + /** + * Limits the maximum number of characters that can be entered. Use this + * instead of implementing the logic in JS to avoid flicker. +diff --git a/node_modules/react-native/Libraries/Components/TextInput/TextInput.js b/node_modules/react-native/Libraries/Components/TextInput/TextInput.js +index 8cfde15..4f3345c 100644 +--- a/node_modules/react-native/Libraries/Components/TextInput/TextInput.js ++++ b/node_modules/react-native/Libraries/Components/TextInput/TextInput.js +@@ -731,6 +731,12 @@ export type Props = $ReadOnly<{| + */ + maxFontSizeMultiplier?: ?number, + ++ /** ++ * Restricts the text value to match the specified regular expression. Use this ++ * instead of implementing the logic in JS to avoid flicker. ++ */ ++ regex?: ?string, ++ + /** + * Limits the maximum number of characters that can be entered. Use this + * instead of implementing the logic in JS to avoid flicker. +diff --git a/node_modules/react-native/Libraries/Text/TextInput/RCTBaseTextInputViewManager.mm b/node_modules/react-native/Libraries/Text/TextInput/RCTBaseTextInputViewManager.mm +index e367394..95f21f2 100644 +--- a/node_modules/react-native/Libraries/Text/TextInput/RCTBaseTextInputViewManager.mm ++++ b/node_modules/react-native/Libraries/Text/TextInput/RCTBaseTextInputViewManager.mm +@@ -59,6 +59,7 @@ @implementation RCTBaseTextInputViewManager { + RCT_EXPORT_VIEW_PROPERTY(inputAccessoryViewID, NSString) + RCT_EXPORT_VIEW_PROPERTY(textContentType, NSString) + RCT_EXPORT_VIEW_PROPERTY(passwordRules, NSString) ++RCT_EXPORT_VIEW_PROPERTY(regex, NSString) + + RCT_EXPORT_VIEW_PROPERTY(onChange, RCTBubblingEventBlock) + RCT_EXPORT_VIEW_PROPERTY(onKeyPressSync, RCTDirectEventBlock) +diff --git a/node_modules/react-native/React/Fabric/Mounting/ComponentViews/TextInput/RCTTextInputComponentView.mm b/node_modules/react-native/React/Fabric/Mounting/ComponentViews/TextInput/RCTTextInputComponentView.mm +index db7cba4..f85f95a 100644 +--- a/node_modules/react-native/React/Fabric/Mounting/ComponentViews/TextInput/RCTTextInputComponentView.mm ++++ b/node_modules/react-native/React/Fabric/Mounting/ComponentViews/TextInput/RCTTextInputComponentView.mm +@@ -34,6 +34,7 @@ @implementation RCTTextInputComponentView { + UIView *_backedTextInputView; + NSUInteger _mostRecentEventCount; + NSAttributedString *_lastStringStateWasUpdatedWith; ++ NSRegularExpression *_regex; + + /* + * UIKit uses either UITextField or UITextView as its UIKit element for . UITextField is for single line +@@ -224,6 +225,13 @@ - (void)updateProps:(const Props::Shared &)props oldProps:(const Props::Shared & + if (newTextInputProps.inputAccessoryViewID != oldTextInputProps.inputAccessoryViewID) { + _backedTextInputView.inputAccessoryViewID = RCTNSStringFromString(newTextInputProps.inputAccessoryViewID); + } ++ ++ if (newTextInputProps.regex != oldTextInputProps.regex) { ++ _regex = [NSRegularExpression regularExpressionWithPattern:RCTNSStringFromString(newTextInputProps.regex) ++ options:0 ++ error:nil]; ++ } ++ + [super updateProps:props oldProps:oldProps]; + + [self setDefaultInputAccessoryView]; +@@ -359,6 +367,14 @@ - (NSString *)textInputShouldChangeText:(NSString *)text inRange:(NSRange)range + } + } + ++ if (_regex) { ++ NSMutableString *newString = [_backedTextInputView.attributedText.string mutableCopy]; ++ [newString replaceCharactersInRange:range withString:text]; ++ if ([_regex numberOfMatchesInString:newString options:0 range:NSMakeRange(0, newString.length)] == 0) { ++ return nil; ++ } ++ } ++ + if (props.maxLength) { + NSInteger allowedLength = props.maxLength - _backedTextInputView.attributedText.string.length + range.length; + +diff --git a/node_modules/react-native/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactTextInputManager.java b/node_modules/react-native/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactTextInputManager.java +index 2cceb14..8fdc0c1 100644 +--- a/node_modules/react-native/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactTextInputManager.java ++++ b/node_modules/react-native/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactTextInputManager.java +@@ -824,6 +824,47 @@ public class ReactTextInputManager extends BaseViewManager 0) { ++ LinkedList list = new LinkedList<>(); ++ for (InputFilter currentFilter : currentFilters) { ++ if (!(currentFilter instanceof RegexFilter)) { ++ list.add(currentFilter); ++ } ++ } ++ if (!list.isEmpty()) { ++ newFilters = (InputFilter[]) list.toArray(new InputFilter[list.size()]); ++ } ++ } ++ } else { ++ if (currentFilters.length > 0) { ++ newFilters = currentFilters; ++ boolean replaced = false; ++ for (int i = 0; i < currentFilters.length; i++) { ++ if (currentFilters[i] instanceof RegexFilter) { ++ currentFilters[i] = new RegexFilter(regex); ++ replaced = true; ++ } ++ } ++ if (!replaced) { ++ newFilters = new InputFilter[currentFilters.length + 1]; ++ System.arraycopy(currentFilters, 0, newFilters, 0, currentFilters.length); ++ newFilters[currentFilters.length] = new RegexFilter(regex); ++ } ++ } else { ++ newFilters = new InputFilter[1]; ++ newFilters[0] = new RegexFilter(regex); ++ } ++ } ++ ++ view.setFilters(newFilters); ++ } ++ + @ReactProp(name = "maxLength") + public void setMaxLength(ReactEditText view, @Nullable Integer maxLength) { + InputFilter[] currentFilters = view.getFilters(); +@@ -854,7 +895,7 @@ public class ReactTextInputManager extends BaseViewManager *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', @@ -506,10 +621,9 @@ const CONST = { DIRECT_FEEDS: 'directFeeds', NETSUITE_USA_TAX: 'netsuiteUsaTax', NEW_DOT_COPILOT: 'newDotCopilot', - WORKSPACE_RULES: 'workspaceRules', COMBINED_TRACK_SUBMIT: 'combinedTrackSubmit', CATEGORY_AND_TAG_APPROVERS: 'categoryAndTagApprovers', - NEW_DOT_QBD: 'quickbooksDesktopOnNewDot', + PER_DIEM: 'newDotPerDiem', }, BUTTON_STATES: { DEFAULT: 'default', @@ -542,6 +656,7 @@ const CONST = { ANDROID: 'android', WEB: 'web', DESKTOP: 'desktop', + MOBILEWEB: 'mobileweb', }, PLATFORM_SPECIFIC_KEYS: { CTRL: { @@ -1099,6 +1214,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', @@ -1146,9 +1262,6 @@ const CONST = { SEARCH_OPTION_LIST_DEBOUNCE_TIME: 300, RESIZE_DEBOUNCE_TIME: 100, UNREAD_UPDATE_DEBOUNCE_TIME: 300, - SEARCH_CONVERT_SEARCH_VALUES: 'search_convert_search_values', - SEARCH_MAKE_TREE: 'search_make_tree', - SEARCH_BUILD_TREE: 'search_build_tree', SEARCH_FILTER_OPTIONS: 'search_filter_options', USE_DEBOUNCED_STATE_DELAY: 300, }, @@ -1182,7 +1295,13 @@ const CONST = { PENDING: 'Pending', POSTED: 'Posted', }, + STATE: { + CURRENT: 'current', + DRAFT: 'draft', + BACKUP: 'backup', + }, }, + MCC_GROUPS: { AIRLINES: 'Airlines', COMMUTER: 'Commuter', @@ -2580,6 +2699,11 @@ const CONST = { INDIVIDUAL: 'individual', NONE: 'none', }, + VERIFICATION_STATE: { + LOADING: 'loading', + VERIFIED: 'verified', + ON_WAITLIST: 'onWaitlist', + }, STATE: { STATE_NOT_ISSUED: 2, OPEN: 3, @@ -2594,6 +2718,7 @@ const CONST = { MONTHLY: 'monthly', FIXED: 'fixed', }, + LIMIT_VALUE: 21474836, STEP_NAMES: ['1', '2', '3', '4', '5', '6'], STEP: { ASSIGNEE: 'Assignee', @@ -2841,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}`); @@ -2929,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, @@ -3325,6 +3452,63 @@ const CONST = { ZW: 'Zimbabwe', }, + ALL_EUROPEAN_COUNTRIES: { + AL: 'Albania', + AD: 'Andorra', + AT: 'Austria', + BY: 'Belarus', + BE: 'Belgium', + BA: 'Bosnia & Herzegovina', + BG: 'Bulgaria', + HR: 'Croatia', + CY: 'Cyprus', + CZ: 'Czech Republic', + DK: 'Denmark', + EE: 'Estonia', + FO: 'Faroe Islands', + FI: 'Finland', + FR: 'France', + GE: 'Georgia', + DE: 'Germany', + GI: 'Gibraltar', + GR: 'Greece', + GL: 'Greenland', + HU: 'Hungary', + IS: 'Iceland', + IE: 'Ireland', + IM: 'Isle of Man', + IT: 'Italy', + JE: 'Jersey', + XK: 'Kosovo', + LV: 'Latvia', + LI: 'Liechtenstein', + LT: 'Lithuania', + LU: 'Luxembourg', + MT: 'Malta', + MD: 'Moldova', + MC: 'Monaco', + ME: 'Montenegro', + NL: 'Netherlands', + MK: 'North Macedonia', + NO: 'Norway', + PL: 'Poland', + PT: 'Portugal', + RO: 'Romania', + RU: 'Russia', + SM: 'San Marino', + RS: 'Serbia', + SK: 'Slovakia', + SI: 'Slovenia', + ES: 'Spain', + SJ: 'Svalbard & Jan Mayen', + SE: 'Sweden', + CH: 'Switzerland', + TR: 'Turkey', + UA: 'Ukraine', + GB: 'United Kingdom', + VA: 'Vatican City', + }, + // Sources: https://github.com/Expensify/App/issues/14958#issuecomment-1442138427 // https://github.com/Expensify/App/issues/14958#issuecomment-1456026810 COUNTRY_ZIP_REGEX_DATA: { @@ -4186,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', @@ -4619,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}, @@ -4662,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, @@ -4774,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: { @@ -4872,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, @@ -5746,6 +5935,11 @@ const CONST = { IN: 'in', }, EMPTY_VALUE: 'none', + SEARCH_ROUTER_ITEM_TYPE: { + CONTEXTUAL_SUGGESTION: 'contextualSuggestion', + AUTOCOMPLETE_SUGGESTION: 'autocompleteSuggestion', + SEARCH: 'searchItem', + }, }, REFERRER: { @@ -5960,6 +6154,7 @@ const CONST = { HAS_WALLET_TERMS_ERRORS: 'hasWalletTermsErrors', HAS_LOGIN_LIST_INFO: 'hasLoginListInfo', HAS_SUBSCRIPTION_INFO: 'hasSubscriptionInfo', + HAS_PHONE_NUMBER_ERROR: 'hasPhoneNumberError', }, DEBUG: { 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 c346da6cadcb..45501bf46374 100644 --- a/src/ROUTES.ts +++ b/src/ROUTES.ts @@ -208,6 +208,7 @@ const ROUTES = { }, SETTINGS_LEGAL_NAME: 'settings/profile/legal-name', SETTINGS_DATE_OF_BIRTH: 'settings/profile/date-of-birth', + SETTINGS_PHONE_NUMBER: 'settings/profile/phone', SETTINGS_ADDRESS: 'settings/profile/address', SETTINGS_ADDRESS_COUNTRY: { route: 'settings/profile/address/country', @@ -950,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 2e44c5ed5695..dea0f028e1a0 100644 --- a/src/SCREENS.ts +++ b/src/SCREENS.ts @@ -84,6 +84,7 @@ const SCREENS = { TIMEZONE_SELECT: 'Settings_Timezone_Select', LEGAL_NAME: 'Settings_LegalName', DATE_OF_BIRTH: 'Settings_DateOfBirth', + PHONE_NUMBER: 'Settings_PhoneNumber', ADDRESS: 'Settings_Address', ADDRESS_COUNTRY: 'Settings_Address_Country', ADDRESS_STATE: 'Settings_Address_State', @@ -531,6 +532,7 @@ const SCREENS = { DISTANCE_RATE_TAX_RECLAIMABLE_ON_EDIT: 'Distance_Rate_Tax_Reclaimable_On_Edit', DISTANCE_RATE_TAX_RATE_EDIT: 'Distance_Rate_Tax_Rate_Edit', UPGRADE: 'Workspace_Upgrade', + DOWNGRADE: 'Workspace_Downgrade', RULES: 'Policy_Rules', RULES_CUSTOM_NAME: 'Rules_Custom_Name', RULES_AUTO_APPROVE_REPORTS_UNDER: 'Rules_Auto_Approve_Reports_Under', diff --git a/src/components/AccountSwitcher.tsx b/src/components/AccountSwitcher.tsx index 8ccab44a2cb9..ad58294c0cc8 100644 --- a/src/components/AccountSwitcher.tsx +++ b/src/components/AccountSwitcher.tsx @@ -152,7 +152,7 @@ function AccountSwitcher() { > {currentUserPersonalDetails?.displayName} - {canSwitchAccounts && ( + {!!canSwitchAccounts && ( - {canSwitchAccounts && ( + {!!canSwitchAccounts && ( { diff --git a/src/components/AmountForm.tsx b/src/components/AmountForm.tsx index 4848577bdea0..de3a1fe39829 100644 --- a/src/components/AmountForm.tsx +++ b/src/components/AmountForm.tsx @@ -238,6 +238,7 @@ function AmountForm( forwardDeletePressedRef.current = key === 'delete' || (allowedOS.includes(operatingSystem ?? '') && event.nativeEvent.ctrlKey && key === 'd'); }; + const regex = useMemo(() => MoneyRequestUtils.amountRegex(decimals, amountMaxLength), [decimals, amountMaxLength]); const formattedAmount = MoneyRequestUtils.replaceAllDigits(currentAmount, toLocaleDigit); const canUseTouchScreen = DeviceCapabilities.canUseTouchScreen(); @@ -261,6 +262,7 @@ function AmountForm( keyboardType={CONST.KEYBOARD_TYPE.DECIMAL_PAD} inputMode={CONST.INPUT_MODE.DECIMAL} errorText={errorText} + regex={regex} // eslint-disable-next-line react/jsx-props-no-spreading {...rest} /> @@ -300,6 +302,7 @@ function AmountForm( isCurrencyPressable={isCurrencyPressable} style={[styles.iouAmountTextInput]} containerStyle={[styles.iouAmountTextInputContainer]} + regex={regex} // eslint-disable-next-line react/jsx-props-no-spreading {...rest} /> diff --git a/src/components/AmountTextInput.tsx b/src/components/AmountTextInput.tsx index 52c32ce1f584..2e0d3e62afa0 100644 --- a/src/components/AmountTextInput.tsx +++ b/src/components/AmountTextInput.tsx @@ -39,7 +39,7 @@ type AmountTextInputProps = { /** Hide the focus styles on TextInput */ hideFocusedState?: boolean; -} & Pick; +} & Pick; function AmountTextInput( { diff --git a/src/components/AmountWithoutCurrencyForm.tsx b/src/components/AmountWithoutCurrencyForm.tsx index 78b7c84ecb54..6a9fc22f68f8 100644 --- a/src/components/AmountWithoutCurrencyForm.tsx +++ b/src/components/AmountWithoutCurrencyForm.tsx @@ -1,7 +1,8 @@ -import React, {useCallback, useMemo} from 'react'; +import React, {useCallback, useMemo, useState} from 'react'; import type {ForwardedRef} from 'react'; +import type {NativeSyntheticEvent, TextInputSelectionChangeEventData} from 'react-native'; import useLocalize from '@hooks/useLocalize'; -import {addLeadingZero, replaceAllDigits, replaceCommasWithPeriod, stripSpacesFromAmount, validateAmount} from '@libs/MoneyRequestUtils'; +import {addLeadingZero, amountRegex, replaceAllDigits, replaceCommasWithPeriod, stripSpacesFromAmount, validateAmount} from '@libs/MoneyRequestUtils'; import CONST from '@src/CONST'; import TextInput from './TextInput'; import type {BaseTextInputProps, BaseTextInputRef} from './TextInput/BaseTextInput/types'; @@ -21,6 +22,11 @@ function AmountWithoutCurrencyForm( const {toLocaleDigit} = useLocalize(); const currentAmount = useMemo(() => (typeof amount === 'string' ? amount : ''), [amount]); + const [selection, setSelection] = useState({ + start: currentAmount.length, + end: currentAmount.length, + }); + const decimals = 2; /** * Sets the selection and the amount accordingly to the value passed to the input @@ -33,7 +39,10 @@ function AmountWithoutCurrencyForm( const newAmountWithoutSpaces = stripSpacesFromAmount(newAmount); const replacedCommasAmount = replaceCommasWithPeriod(newAmountWithoutSpaces); const withLeadingZero = addLeadingZero(replacedCommasAmount); - if (!validateAmount(withLeadingZero, 2)) { + if (!validateAmount(withLeadingZero, decimals)) { + // Use a shallow copy of selection to trigger setSelection + // More info: https://github.com/Expensify/App/issues/16385 + setSelection((prevSelection) => ({...prevSelection})); return; } onInputChange?.(withLeadingZero); @@ -41,12 +50,17 @@ function AmountWithoutCurrencyForm( [onInputChange], ); + const regex = useMemo(() => amountRegex(decimals), [decimals]); const formattedAmount = replaceAllDigits(currentAmount, toLocaleDigit); return ( ) => { + setSelection(e.nativeEvent.selection); + }} inputID={inputID} name={name} label={label} @@ -55,6 +69,7 @@ function AmountWithoutCurrencyForm( role={role} ref={ref} keyboardType={CONST.KEYBOARD_TYPE.DECIMAL_PAD} + regex={regex} // eslint-disable-next-line react/jsx-props-no-spreading {...rest} /> diff --git a/src/components/AttachmentPicker/index.native.tsx b/src/components/AttachmentPicker/index.native.tsx index d3a51c7fc0f0..bc4467e82f01 100644 --- a/src/components/AttachmentPicker/index.native.tsx +++ b/src/components/AttachmentPicker/index.native.tsx @@ -33,46 +33,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 +111,14 @@ 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 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 +144,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 +201,7 @@ function AttachmentPicker({ } }); }), - [showGeneralAlert, type], + [fileLimit, showGeneralAlert, type], ); /** * Launch the DocumentPicker. Results are in the same format as ImagePicker @@ -209,7 +210,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 +218,7 @@ function AttachmentPicker({ showGeneralAlert(error.message); throw error; }), - [showGeneralAlert, type], + [fileLimit, showGeneralAlert, type], ); const menuItemData: Item[] = useMemo(() => { @@ -261,7 +262,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 +287,7 @@ function AttachmentPicker({ } return getDataForUpload(fileData) .then((result) => { - completeAttachmentSelection.current(result); + completeAttachmentSelection.current([result]); }) .catch((error: Error) => { showGeneralAlert(error.message); @@ -301,63 +302,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], ); /** 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