diff --git a/.eslintrc.js b/.eslintrc.js index 3f0bce754e16..1547b34f8d54 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -2,10 +2,6 @@ module.exports = { extends: 'expensify', parser: 'babel-eslint', ignorePatterns: ['!.*', 'src/vendor', '.github/actions/**/index.js'], - rules: { - 'react/jsx-filename-extension': [1, {extensions: ['.js']}], - 'comma-dangle': ['error', 'always-multiline'], - }, plugins: ['detox'], env: { jest: true, diff --git a/.github/actions/bumpVersion/index.js b/.github/actions/bumpVersion/index.js index f08bb2a0cf32..23f8708f646d 100644 --- a/.github/actions/bumpVersion/index.js +++ b/.github/actions/bumpVersion/index.js @@ -181,7 +181,9 @@ exports.updateiOSVersion = function updateiOSVersion(version) { /***/ }), /***/ 7: -/***/ ((module) => { +/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { + +const _ = __nccwpck_require__(571); const SEMANTIC_VERSION_LEVELS = { MAJOR: 'MAJOR', @@ -199,7 +201,7 @@ const MAX_INCREMENTS = 99; */ const getVersionNumberFromString = (versionString) => { const [version, build] = versionString.split('-'); - const [major, minor, patch] = version.split('.').map(n => Number(n)); + const [major, minor, patch] = _.map(version.split('.'), n => Number(n)); return [major, minor, patch, Number.isInteger(Number(build)) ? Number(build) : 0]; }; diff --git a/.github/actions/isPullRequestMergeable/index.js b/.github/actions/isPullRequestMergeable/index.js index a03b65a8974c..8a40171bd26f 100644 --- a/.github/actions/isPullRequestMergeable/index.js +++ b/.github/actions/isPullRequestMergeable/index.js @@ -29,12 +29,14 @@ const run = function () { pull_number: pullRequestNumber, }) .then(({data}) => { - if (!_.isNull(data.mergeable)) { - console.log('Pull request mergeability is not yet resolved...'); - retryCount++; - mergeabilityResolved = true; - isMergeable = data.mergeable; + if (_.isNull(data.mergeable)) { + return; } + + console.log('Pull request mergeability is not yet resolved...'); + retryCount++; + mergeabilityResolved = true; + isMergeable = data.mergeable; }) .catch((githubError) => { mergeabilityResolved = true; diff --git a/.github/actions/isPullRequestMergeable/isPullRequestMergeable.js b/.github/actions/isPullRequestMergeable/isPullRequestMergeable.js index 71be6f90e2a2..1498bf7afc6c 100644 --- a/.github/actions/isPullRequestMergeable/isPullRequestMergeable.js +++ b/.github/actions/isPullRequestMergeable/isPullRequestMergeable.js @@ -19,12 +19,14 @@ const run = function () { pull_number: pullRequestNumber, }) .then(({data}) => { - if (!_.isNull(data.mergeable)) { - console.log('Pull request mergeability is not yet resolved...'); - retryCount++; - mergeabilityResolved = true; - isMergeable = data.mergeable; + if (_.isNull(data.mergeable)) { + return; } + + console.log('Pull request mergeability is not yet resolved...'); + retryCount++; + mergeabilityResolved = true; + isMergeable = data.mergeable; }) .catch((githubError) => { mergeabilityResolved = true; diff --git a/.github/actions/markPullRequestsAsDeployed/index.js b/.github/actions/markPullRequestsAsDeployed/index.js index 8396d17e27cc..b7dcf4123709 100644 --- a/.github/actions/markPullRequestsAsDeployed/index.js +++ b/.github/actions/markPullRequestsAsDeployed/index.js @@ -97,7 +97,7 @@ const run = function () { .then((actor) => { // Create comment on each pull request (one after another to avoid throttling issues) const deployMessage = getDeployMessage(actor, 'Deployed'); - prList.reduce((promise, pr) => promise.then(() => commentPR(pr, deployMessage)), Promise.resolve()); + _.reduce(prList, (promise, pr) => promise.then(() => commentPR(pr, deployMessage)), Promise.resolve()); }); } @@ -117,7 +117,7 @@ const run = function () { }) .then(({data}) => { const isCP = /Merge pull request #\d+ from Expensify\/.*-?cherry-pick-staging-\d+/.test(data.message); - prList.reduce((promise, PR) => promise + _.reduce(prList, (promise, PR) => promise // Then, for each PR, find out who merged it and determine the deployer .then(() => GithubUtils.octokit.pulls.get({ diff --git a/.github/actions/markPullRequestsAsDeployed/markPullRequestsAsDeployed.js b/.github/actions/markPullRequestsAsDeployed/markPullRequestsAsDeployed.js index 54fdedf286ac..b89a90bd4a3e 100644 --- a/.github/actions/markPullRequestsAsDeployed/markPullRequestsAsDeployed.js +++ b/.github/actions/markPullRequestsAsDeployed/markPullRequestsAsDeployed.js @@ -87,7 +87,7 @@ const run = function () { .then((actor) => { // Create comment on each pull request (one after another to avoid throttling issues) const deployMessage = getDeployMessage(actor, 'Deployed'); - prList.reduce((promise, pr) => promise.then(() => commentPR(pr, deployMessage)), Promise.resolve()); + _.reduce(prList, (promise, pr) => promise.then(() => commentPR(pr, deployMessage)), Promise.resolve()); }); } @@ -107,7 +107,7 @@ const run = function () { }) .then(({data}) => { const isCP = /Merge pull request #\d+ from Expensify\/.*-?cherry-pick-staging-\d+/.test(data.message); - prList.reduce((promise, PR) => promise + _.reduce(prList, (promise, PR) => promise // Then, for each PR, find out who merged it and determine the deployer .then(() => GithubUtils.octokit.pulls.get({ diff --git a/.github/libs/versionUpdater.js b/.github/libs/versionUpdater.js index b0baf1dffdc3..dc5d75c792ff 100644 --- a/.github/libs/versionUpdater.js +++ b/.github/libs/versionUpdater.js @@ -1,3 +1,5 @@ +const _ = require('underscore'); + const SEMANTIC_VERSION_LEVELS = { MAJOR: 'MAJOR', MINOR: 'MINOR', @@ -14,7 +16,7 @@ const MAX_INCREMENTS = 99; */ const getVersionNumberFromString = (versionString) => { const [version, build] = versionString.split('-'); - const [major, minor, patch] = version.split('.').map(n => Number(n)); + const [major, minor, patch] = _.map(version.split('.'), n => Number(n)); return [major, minor, patch, Number.isInteger(Number(build)) ? Number(build) : 0]; }; diff --git a/.storybook/webpack.config.js b/.storybook/webpack.config.js index 6590a3665170..4b788fe4bf86 100644 --- a/.storybook/webpack.config.js +++ b/.storybook/webpack.config.js @@ -2,6 +2,7 @@ /* eslint-disable no-param-reassign */ const path = require('path'); const dotenv = require('dotenv'); +const _ = require('underscore'); const custom = require('../config/webpack/webpack.common'); const env = dotenv.config({path: path.resolve(__dirname, '../.env.staging')}).parsed; @@ -14,16 +15,16 @@ module.exports = ({config}) => { }; // Necessary to overwrite the values in the existing DefinePlugin hardcoded to the Config staging values - const definePluginIndex = config.plugins.findIndex(plugin => plugin.constructor.name === 'DefinePlugin'); + const definePluginIndex = _.findIndex(config.plugins, plugin => plugin.constructor.name === 'DefinePlugin'); config.plugins[definePluginIndex].definitions.__REACT_WEB_CONFIG__ = JSON.stringify(env); config.resolve.extensions.push('.web.js', '.website.js'); - const babelRulesIndex = custom.module.rules.findIndex(rule => rule.loader === 'babel-loader'); + const babelRulesIndex = _.findIndex(custom.module.rules, rule => rule.loader === 'babel-loader'); const babelRule = custom.module.rules[babelRulesIndex]; config.module.rules.push(babelRule); // Allows loading SVG - more context here https://github.com/storybookjs/storybook/issues/6188 - const fileLoaderRule = config.module.rules.find(rule => rule.test && rule.test.test('.svg')); + const fileLoaderRule = _.find(config.module.rules, rule => rule.test && rule.test.test('.svg')); fileLoaderRule.exclude = /\.svg$/; config.module.rules.push({ test: /\.svg$/, diff --git a/config/checkMetroBundlerPort.js b/config/checkMetroBundlerPort.js index 4c8d017fc1e0..a6474c891706 100644 --- a/config/checkMetroBundlerPort.js +++ b/config/checkMetroBundlerPort.js @@ -8,11 +8,13 @@ const {isPackagerRunning} = require('@react-native-community/cli-tools'); * - `unrecognized`: one other process is running on the port we expect the packager to be running. */ isPackagerRunning().then((result) => { - if (result === 'unrecognized') { - console.error( - 'The port 8081 is currently in use.', - 'You can run `lsof -i :8081` to see which program is using it.\n', - ); - process.exit(1); + if (result !== 'unrecognized') { + return; } + + console.error( + 'The port 8081 is currently in use.', + 'You can run `lsof -i :8081` to see which program is using it.\n', + ); + process.exit(1); }); diff --git a/config/webpack/webpack.common.js b/config/webpack/webpack.common.js index 45a2e6b73bea..997498cc9211 100644 --- a/config/webpack/webpack.common.js +++ b/config/webpack/webpack.common.js @@ -1,3 +1,4 @@ +const _ = require('underscore'); const path = require('path'); const {IgnorePlugin} = require('webpack'); const {CleanWebpackPlugin} = require('clean-webpack-plugin'); @@ -7,7 +8,7 @@ const CustomVersionFilePlugin = require('./CustomVersionFilePlugin'); // Check for a --platform command line argument (default to 'web') // If it is 'web', we want to ignore .desktop.js files, and if it is 'desktop', we want to ignore .website.js files. -const platformIndex = process.argv.findIndex(arg => arg === '--platform'); +const platformIndex = _.findIndex(process.argv, arg => arg === '--platform'); const platform = (platformIndex > 0) ? process.argv[platformIndex + 1] : 'web'; const platformExclude = platform === 'web' ? new RegExp(/\.desktop\.js$/) : new RegExp(/\.website\.js$/); diff --git a/config/webpack/webpack.dev.js b/config/webpack/webpack.dev.js index 974be2ca6351..e32304af8aac 100644 --- a/config/webpack/webpack.dev.js +++ b/config/webpack/webpack.dev.js @@ -2,7 +2,7 @@ const path = require('path'); const webpack = require('webpack'); const {merge} = require('webpack-merge'); const dotenv = require('dotenv'); -const common = require('./webpack.common.js'); +const common = require('./webpack.common'); const env = dotenv.config({path: path.resolve(__dirname, '../../.env')}).parsed; diff --git a/config/webpack/webpack.prod.js b/config/webpack/webpack.prod.js index c9b3bdb6d007..dfd7c852ab99 100644 --- a/config/webpack/webpack.prod.js +++ b/config/webpack/webpack.prod.js @@ -1,7 +1,7 @@ const path = require('path'); const {merge} = require('webpack-merge'); const dotenv = require('dotenv'); -const common = require('./webpack.common.js'); +const common = require('./webpack.common'); const getProductionConfig = require('./productionConfig'); const env = dotenv.config({path: path.resolve(__dirname, '../../.env.production')}).parsed; diff --git a/config/webpack/webpack.staging.js b/config/webpack/webpack.staging.js index 50469af3dc45..24f706d90731 100644 --- a/config/webpack/webpack.staging.js +++ b/config/webpack/webpack.staging.js @@ -1,7 +1,7 @@ const path = require('path'); const {merge} = require('webpack-merge'); const dotenv = require('dotenv'); -const common = require('./webpack.common.js'); +const common = require('./webpack.common'); const getProductionConfig = require('./productionConfig'); const env = dotenv.config({path: path.resolve(__dirname, '../../.env.staging')}).parsed; diff --git a/desktop/main.js b/desktop/main.js index c19db1ce5c36..f77020f1b980 100644 --- a/desktop/main.js +++ b/desktop/main.js @@ -6,6 +6,7 @@ const { shell, ipcMain, } = require('electron'); +const _ = require('underscore'); const serve = require('electron-serve'); const contextMenu = require('electron-context-menu'); const {autoUpdater} = require('electron-updater'); @@ -41,7 +42,7 @@ autoUpdater.logger.transports.file.level = 'info'; // Send all Console logs to a log file: ~/Library/Logs/new.expensify/main.log // See https://www.npmjs.com/package/electron-log -Object.assign(console, log.functions); +_.assign(console, log.functions); // setup Hot reload if (isDev) { @@ -155,18 +156,18 @@ const mainWindow = (() => { }], })); - const appMenu = systemMenu.items.find(item => item.role === 'appmenu'); + const appMenu = _.find(systemMenu.items, item => item.role === 'appmenu'); appMenu.submenu.insert(1, updateAppMenuItem); // On mac, pressing cmd++ actually sends a cmd+=. cmd++ is generally the zoom in shortcut, but this is // not properly listened for by electron. Adding in an invisible cmd+= listener fixes this. - const viewWindow = systemMenu.items.find(item => item.role === 'viewmenu'); + const viewWindow = _.find(systemMenu.items, item => item.role === 'viewmenu'); viewWindow.submenu.append(new MenuItem({ role: 'zoomin', accelerator: 'CommandOrControl+=', visible: false, })); - const windowMenu = systemMenu.items.find(item => item.role === 'windowmenu'); + const windowMenu = _.find(systemMenu.items, item => item.role === 'windowmenu'); windowMenu.submenu.append(new MenuItem({type: 'separator'})); windowMenu.submenu.append(new MenuItem({ label: 'New Expensify', @@ -193,10 +194,12 @@ const mainWindow = (() => { // Closing the chat window should just hide it (vs. fully quitting the application) browserWindow.on('close', (evt) => { - if (!quitting && !hasUpdate) { - evt.preventDefault(); - browserWindow.hide(); + if (quitting || hasUpdate) { + return; } + + evt.preventDefault(); + browserWindow.hide(); }); // Initiating a browser-back or browser-forward with mouse buttons should navigate history. @@ -211,9 +214,11 @@ const mainWindow = (() => { app.on('before-quit', () => quitting = true); app.on('activate', () => { - if (!expectedUpdateVersion || app.getVersion() === expectedUpdateVersion) { - browserWindow.show(); + if (expectedUpdateVersion && app.getVersion() !== expectedUpdateVersion) { + return; } + + browserWindow.show(); }); // Hide the app if we expected to upgrade to a new version but never did. @@ -259,9 +264,11 @@ const mainWindow = (() => { // Start checking for JS updates .then((browserWindow) => { - if (!isDev) { - checkForUpdates(electronUpdater(browserWindow)); + if (isDev) { + return; } + + checkForUpdates(electronUpdater(browserWindow)); }); }); diff --git a/metro.config.js b/metro.config.js index c135148c904d..19663bd0e449 100644 --- a/metro.config.js +++ b/metro.config.js @@ -4,6 +4,7 @@ */ const {getDefaultConfig} = require('metro-config'); +const _ = require('underscore'); /* eslint arrow-body-style: 0 */ module.exports = (() => { @@ -11,7 +12,7 @@ module.exports = (() => { .then((config) => { return { resolver: { - assetExts: config.resolver.assetExts.filter(ext => ext !== 'svg'), + assetExts: _.filter(config.resolver.assetExts, ext => ext !== 'svg'), sourceExts: ['jsx', 'js', 'ts', 'tsx', 'json', 'svg'], }, transformer: { diff --git a/package-lock.json b/package-lock.json index 23e683df8bcf..742f74d3c0ba 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14774,6 +14774,12 @@ "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.6.tgz", "integrity": "sha512-3c+yGKvVP5Y9TYBEibGNR+kLtijnj7mYrXRg+WpFb2X9xm04g/DXYkfg4hmzJQosc9snFNUPkbYIhu+KAm6jJw==" }, + "@types/json5": { + "version": "0.0.29", + "resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz", + "integrity": "sha1-7ihweulOEdK4J7y+UnC86n8+ce4=", + "dev": true + }, "@types/markdown-to-jsx": { "version": "6.11.3", "resolved": "https://registry.npmjs.org/@types/markdown-to-jsx/-/markdown-to-jsx-6.11.3.tgz", @@ -19129,12 +19135,6 @@ "resolved": "https://registry.npmjs.org/constants-browserify/-/constants-browserify-1.0.0.tgz", "integrity": "sha1-wguW2MYXdIqvHBYCF2DNJ/y4y3U=" }, - "contains-path": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/contains-path/-/contains-path-0.1.0.tgz", - "integrity": "sha1-/ozxhP9mcLa67wGp1IYaXL7EEgo=", - "dev": true - }, "content-disposition": { "version": "0.5.3", "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.3.tgz", @@ -21989,9 +21989,9 @@ } }, "eslint-config-expensify": { - "version": "2.0.16", - "resolved": "https://registry.npmjs.org/eslint-config-expensify/-/eslint-config-expensify-2.0.16.tgz", - "integrity": "sha512-tN7cns6nVREB4EhJnt+KoJYWafdSKfCPq/9qdoZfgUjxsxrZLnjLHjjW1iiXfA/9WRXDUu3g2qFIGDPnP/IRIw==", + "version": "2.0.17", + "resolved": "https://registry.npmjs.org/eslint-config-expensify/-/eslint-config-expensify-2.0.17.tgz", + "integrity": "sha512-m+7Rsz517u+jtkmN9MqMOhQ7cUSJo07Zx7Wt1mCwOw2DjC5DDhx7iiGE12oAtsmQTR+DHMyoUY3cAUXaakeh6w==", "dev": true, "requires": { "@lwc/eslint-plugin-lwc": "^0.11.0", @@ -21999,9 +21999,13 @@ "eslint": "6.8.0", "eslint-config-airbnb": "18.0.1", "eslint-config-airbnb-base": "14.0.0", - "eslint-plugin-import": "2.20.0", + "eslint-plugin-es": "^4.1.0", + "eslint-plugin-import": "^2.25.2", "eslint-plugin-jsx-a11y": "6.2.3", - "eslint-plugin-react": "7.18.0" + "eslint-plugin-react": "7.18.0", + "eslint-plugin-rulesdir": "^0.2.0", + "lodash": "^4.17.21", + "underscore": "^1.13.1" }, "dependencies": { "acorn": { @@ -22027,6 +22031,12 @@ } } }, + "ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true + }, "ansi-styles": { "version": "3.2.1", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", @@ -22298,12 +22308,12 @@ "dev": true }, "strip-ansi": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz", - "integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", "dev": true, "requires": { - "ansi-regex": "^5.0.0" + "ansi-regex": "^5.0.1" } }, "supports-color": { @@ -22474,9 +22484,9 @@ } }, "eslint-import-resolver-node": { - "version": "0.3.5", - "resolved": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.5.tgz", - "integrity": "sha512-XMoPKjSpXbkeJ7ZZ9icLnJMTY5Mc1kZbCakHquaFsXPpyWOwK0TK6CODO+0ca54UoM9LKOxyUNnoVZRl8TeaAg==", + "version": "0.3.6", + "resolved": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.6.tgz", + "integrity": "sha512-0En0w03NRVMn9Uiyn8YRPDKvWjxCWkslUEhGNTdGx15RvPJYQ+lbOlqrlNI2vEAs4pDYK4f/HN2TbDmk5TP0iw==", "dev": true, "requires": { "debug": "^3.2.7", @@ -22592,12 +22602,13 @@ } }, "eslint-module-utils": { - "version": "2.6.2", - "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.6.2.tgz", - "integrity": "sha512-QG8pcgThYOuqxupd06oYTZoNOGaUdTY1PqK+oS6ElF6vs4pBdk/aYxFVQQXzcrAqp9m7cl7lb2ubazX+g16k2Q==", + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.7.1.tgz", + "integrity": "sha512-fjoetBXQZq2tSTWZ9yWVl2KuFrTZZH3V+9iD1V1RfpDgxzJR+mPd/KZmMiA8gbPqdBzpNiEHOuT7IYEWxrH0zQ==", "dev": true, "requires": { "debug": "^3.2.7", + "find-up": "^2.1.0", "pkg-dir": "^2.0.0" }, "dependencies": { @@ -22679,6 +22690,16 @@ "requireindex": "~1.1.0" } }, + "eslint-plugin-es": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-es/-/eslint-plugin-es-4.1.0.tgz", + "integrity": "sha512-GILhQTnjYE2WorX5Jyi5i4dz5ALWxBIdQECVQavL6s7cI76IZTDWleTHkxz/QT3kvcs2QlGHvKLYsSlPOlPXnQ==", + "dev": true, + "requires": { + "eslint-utils": "^2.0.0", + "regexpp": "^3.0.0" + } + }, "eslint-plugin-eslint-comments": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/eslint-plugin-eslint-comments/-/eslint-plugin-eslint-comments-3.2.0.tgz", @@ -22699,25 +22720,50 @@ } }, "eslint-plugin-import": { - "version": "2.20.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.20.0.tgz", - "integrity": "sha512-NK42oA0mUc8Ngn4kONOPsPB1XhbUvNHqF+g307dPV28aknPoiNnKLFd9em4nkswwepdF5ouieqv5Th/63U7YJQ==", + "version": "2.25.2", + "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.25.2.tgz", + "integrity": "sha512-qCwQr9TYfoBHOFcVGKY9C9unq05uOxxdklmBXLVvcwo68y5Hta6/GzCZEMx2zQiu0woKNEER0LE7ZgaOfBU14g==", "dev": true, "requires": { - "array-includes": "^3.0.3", - "array.prototype.flat": "^1.2.1", - "contains-path": "^0.1.0", + "array-includes": "^3.1.4", + "array.prototype.flat": "^1.2.5", "debug": "^2.6.9", - "doctrine": "1.5.0", - "eslint-import-resolver-node": "^0.3.2", - "eslint-module-utils": "^2.4.1", + "doctrine": "^2.1.0", + "eslint-import-resolver-node": "^0.3.6", + "eslint-module-utils": "^2.7.0", "has": "^1.0.3", + "is-core-module": "^2.7.0", + "is-glob": "^4.0.3", "minimatch": "^3.0.4", - "object.values": "^1.1.0", - "read-pkg-up": "^2.0.0", - "resolve": "^1.12.0" + "object.values": "^1.1.5", + "resolve": "^1.20.0", + "tsconfig-paths": "^3.11.0" }, "dependencies": { + "array-includes": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.4.tgz", + "integrity": "sha512-ZTNSQkmWumEbiHO2GF4GmWxYVTiQyJy2XOTa15sdQSrvKn7l+180egQMqlrMOUMCyLMD7pmyQe4mMDUT6Behrw==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.3", + "es-abstract": "^1.19.1", + "get-intrinsic": "^1.1.1", + "is-string": "^1.0.7" + } + }, + "array.prototype.flat": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.2.5.tgz", + "integrity": "sha512-KaYU+S+ndVqyUnignHftkwc58o3uVU1jzczILJ1tN2YaIZpFIKBiP/x/j97E5MVPsaCloPbqWLB/8qCTVvT2qg==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.3", + "es-abstract": "^1.19.0" + } + }, "debug": { "version": "2.6.9", "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", @@ -22727,133 +22773,175 @@ "ms": "2.0.0" } }, - "doctrine": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-1.5.0.tgz", - "integrity": "sha1-N53Ocw9hZvds76TmcHoVmwLFpvo=", + "es-abstract": { + "version": "1.19.1", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.19.1.tgz", + "integrity": "sha512-2vJ6tjA/UfqLm2MPs7jxVybLoB8i1t1Jd9R3kISld20sIxPcTbLuggQOUxeWeAvIUkduv/CfMjuh4WmiXr2v9w==", "dev": true, "requires": { - "esutils": "^2.0.2", - "isarray": "^1.0.0" + "call-bind": "^1.0.2", + "es-to-primitive": "^1.2.1", + "function-bind": "^1.1.1", + "get-intrinsic": "^1.1.1", + "get-symbol-description": "^1.0.0", + "has": "^1.0.3", + "has-symbols": "^1.0.2", + "internal-slot": "^1.0.3", + "is-callable": "^1.2.4", + "is-negative-zero": "^2.0.1", + "is-regex": "^1.1.4", + "is-shared-array-buffer": "^1.0.1", + "is-string": "^1.0.7", + "is-weakref": "^1.0.1", + "object-inspect": "^1.11.0", + "object-keys": "^1.1.1", + "object.assign": "^4.1.2", + "string.prototype.trimend": "^1.0.4", + "string.prototype.trimstart": "^1.0.4", + "unbox-primitive": "^1.0.1" } }, - "find-up": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-2.1.0.tgz", - "integrity": "sha1-RdG35QbHF93UgndaK3eSCjwMV6c=", + "has-symbols": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.2.tgz", + "integrity": "sha512-chXa79rL/UC2KlX17jo3vRGz0azaWEx5tGqZg5pO3NUyEJVB17dMruQlzCCOfUvElghKcm5194+BCRvi2Rv/Gw==", + "dev": true + }, + "internal-slot": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.3.tgz", + "integrity": "sha512-O0DB1JC/sPyZl7cIo78n5dR7eUSwwpYPiXRhTzNxZVAMUuB8vlnRFyLxdrVToks6XPLVnFfbzaVd5WLjhgg+vA==", "dev": true, "requires": { - "locate-path": "^2.0.0" + "get-intrinsic": "^1.1.0", + "has": "^1.0.3", + "side-channel": "^1.0.4" } }, - "load-json-file": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-2.0.0.tgz", - "integrity": "sha1-eUfkIUmvgNaWy/eXvKq8/h/inKg=", + "is-callable": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.4.tgz", + "integrity": "sha512-nsuwtxZfMX67Oryl9LCQ+upnC0Z0BgpwntpS89m1H/TLF0zNfzfLMV/9Wa/6MZsj0acpEjAO0KF1xT6ZdLl95w==", + "dev": true + }, + "is-core-module": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.8.0.tgz", + "integrity": "sha512-vd15qHsaqrRL7dtH6QNuy0ndJmRDrS9HAM1CAiSifNUFv4x1a0CCVsj18hJ1mShxIG6T2i1sO78MkP56r0nYRw==", "dev": true, "requires": { - "graceful-fs": "^4.1.2", - "parse-json": "^2.2.0", - "pify": "^2.0.0", - "strip-bom": "^3.0.0" + "has": "^1.0.3" } }, - "locate-path": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-2.0.0.tgz", - "integrity": "sha1-K1aLJl7slExtnA3pw9u7ygNUzY4=", + "is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", "dev": true, "requires": { - "p-locate": "^2.0.0", - "path-exists": "^3.0.0" + "is-extglob": "^2.1.1" } }, - "ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", + "is-negative-zero": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.1.tgz", + "integrity": "sha512-2z6JzQvZRa9A2Y7xC6dQQm4FSTSTNWjKIYYTt4246eMTJmIo0Q+ZyOsU66X8lxK1AbB92dFeglPLrhwpeRKO6w==", "dev": true }, - "p-limit": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-1.3.0.tgz", - "integrity": "sha512-vvcXsLAJ9Dr5rQOPk7toZQZJApBl2K4J6dANSsEuh6QI41JYcsS/qhTGa9ErIUUgK3WNQoJYvylxvjqmiqEA9Q==", + "is-regex": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.4.tgz", + "integrity": "sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==", "dev": true, "requires": { - "p-try": "^1.0.0" + "call-bind": "^1.0.2", + "has-tostringtag": "^1.0.0" } }, - "p-locate": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-2.0.0.tgz", - "integrity": "sha1-IKAQOyIqcMj9OcwuWAaA893l7EM=", + "is-string": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.0.7.tgz", + "integrity": "sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg==", "dev": true, "requires": { - "p-limit": "^1.1.0" + "has-tostringtag": "^1.0.0" } }, - "p-try": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/p-try/-/p-try-1.0.0.tgz", - "integrity": "sha1-y8ec26+P1CKOE/Yh8rGiN8GyB7M=", + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", "dev": true }, - "parse-json": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-2.2.0.tgz", - "integrity": "sha1-9ID0BDTvgHQfhGkJn43qGPVaTck=", + "object-inspect": { + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.11.0.tgz", + "integrity": "sha512-jp7ikS6Sd3GxQfZJPyH3cjcbJF6GZPClgdV+EFygjFLQ5FmW/dRUnTd9PQ9k0JhoNDabWFbpF1yCdSWCC6gexg==", + "dev": true + }, + "object.assign": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.2.tgz", + "integrity": "sha512-ixT2L5THXsApyiUPYKmW+2EHpXXe5Ii3M+f4e+aJFAHao5amFRW6J0OO6c/LU8Be47utCx2GL89hxGB6XSmKuQ==", "dev": true, "requires": { - "error-ex": "^1.2.0" + "call-bind": "^1.0.0", + "define-properties": "^1.1.3", + "has-symbols": "^1.0.1", + "object-keys": "^1.1.1" } }, - "path-exists": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", - "integrity": "sha1-zg6+ql94yxiSXqfYENe1mwEP1RU=", - "dev": true - }, - "path-type": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/path-type/-/path-type-2.0.0.tgz", - "integrity": "sha1-8BLMuEFbcJb8LaoQVMPXI4lZTHM=", + "object.values": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.1.5.tgz", + "integrity": "sha512-QUZRW0ilQ3PnPpbNtgdNV1PDbEqLIiSFB3l+EnGtBQ/8SUTLj1PZwtQHABZtLgwpJZTSZhuGLOGk57Drx2IvYg==", "dev": true, "requires": { - "pify": "^2.0.0" + "call-bind": "^1.0.2", + "define-properties": "^1.1.3", + "es-abstract": "^1.19.1" } }, - "pify": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", - "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=", - "dev": true + "resolve": { + "version": "1.20.0", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.20.0.tgz", + "integrity": "sha512-wENBPt4ySzg4ybFQW2TT1zMQucPK95HSh/nq2CFTZVOGut2+pQvSsgtda4d26YrYcr067wjbmzOG8byDPBX63A==", + "dev": true, + "requires": { + "is-core-module": "^2.2.0", + "path-parse": "^1.0.6" + } }, - "read-pkg": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-2.0.0.tgz", - "integrity": "sha1-jvHAYjxqbbDcZxPEv6xGMysjaPg=", + "side-channel": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz", + "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==", "dev": true, "requires": { - "load-json-file": "^2.0.0", - "normalize-package-data": "^2.3.2", - "path-type": "^2.0.0" + "call-bind": "^1.0.0", + "get-intrinsic": "^1.0.2", + "object-inspect": "^1.9.0" } }, - "read-pkg-up": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-2.0.0.tgz", - "integrity": "sha1-a3KoBImE4MQeeVEP1en6mbO1Sb4=", + "string.prototype.trimend": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.4.tgz", + "integrity": "sha512-y9xCjw1P23Awk8EvTpcyL2NIr1j7wJ39f+k6lvRnSMz+mz9CGz9NYPelDk42kOz6+ql8xjfK8oYzy3jAP5QU5A==", "dev": true, "requires": { - "find-up": "^2.0.0", - "read-pkg": "^2.0.0" + "call-bind": "^1.0.2", + "define-properties": "^1.1.3" } }, - "strip-bom": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", - "integrity": "sha1-IzTBjpx1n3vdVv3vfprj1YjmjtM=", - "dev": true + "string.prototype.trimstart": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.4.tgz", + "integrity": "sha512-jh6e984OBfvxS50tdY2nRZnoC5/mLFKOREQfw8t5yytkoUsJRNxvI/E39qu1sD0OtWI3OC0XgKSmcWwziwYuZw==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.3" + } } } }, @@ -23025,6 +23113,12 @@ "integrity": "sha512-9aEPf1JEpiTjcFAmmyw8eiIXmcNZOqaZyHO77wgm0/dWfT/oxC1SrIq8ET38pMxHYrcB6Uew+TzUVsBeczF88g==", "dev": true }, + "eslint-plugin-rulesdir": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-rulesdir/-/eslint-plugin-rulesdir-0.2.0.tgz", + "integrity": "sha512-PPQPCsPkzF3upl1862swPA1bmDAAHKHmJJ4JTHJ11JCVCU4sycB0K5LLA/Rwr6r4VbnpScvUvHV4hqfdjvFmhQ==", + "dev": true + }, "eslint-scope": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", @@ -41070,6 +41164,35 @@ "resolved": "https://registry.npmjs.org/tsc/-/tsc-1.20150623.0.tgz", "integrity": "sha1-Trw8d04WkUjLx2inNCUz8ILHpuU=" }, + "tsconfig-paths": { + "version": "3.11.0", + "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.11.0.tgz", + "integrity": "sha512-7ecdYDnIdmv639mmDwslG6KQg1Z9STTz1j7Gcz0xa+nshh/gKDAHcPxRbWOsA3SPp0tXP2leTcY9Kw+NAkfZzA==", + "dev": true, + "requires": { + "@types/json5": "^0.0.29", + "json5": "^1.0.1", + "minimist": "^1.2.0", + "strip-bom": "^3.0.0" + }, + "dependencies": { + "json5": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.1.tgz", + "integrity": "sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==", + "dev": true, + "requires": { + "minimist": "^1.2.0" + } + }, + "strip-bom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha1-IzTBjpx1n3vdVv3vfprj1YjmjtM=", + "dev": true + } + } + }, "tslib": { "version": "1.14.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", diff --git a/package.json b/package.json index d5b365c898f7..0af2bc9770c7 100644 --- a/package.json +++ b/package.json @@ -146,7 +146,7 @@ "electron-notarize": "^1.0.0", "electron-reloader": "^1.2.0", "eslint": "^7.6.0", - "eslint-config-expensify": "^2.0.16", + "eslint-config-expensify": "2.0.17", "eslint-loader": "^4.0.2", "eslint-plugin-detox": "^1.0.0", "eslint-plugin-jest": "^24.1.0", diff --git a/src/Expensify.js b/src/Expensify.js index 77d7e4f15fb4..78ae766f4be9 100644 --- a/src/Expensify.js +++ b/src/Expensify.js @@ -124,9 +124,11 @@ class Expensify extends PureComponent { } initializeClient() { - if (Visibility.isVisible()) { - ActiveClientManager.init(); + if (!Visibility.isVisible()) { + return; } + + ActiveClientManager.init(); } hideSplash() { diff --git a/src/components/AttachmentPicker/index.js b/src/components/AttachmentPicker/index.js index 477cc8c2a004..c1d666d1d244 100644 --- a/src/components/AttachmentPicker/index.js +++ b/src/components/AttachmentPicker/index.js @@ -8,9 +8,11 @@ import * as attachmentPickerPropTypes from './attachmentPickerPropTypes'; * @returns {String|undefined} Picker will accept all file types when its undefined */ function getAcceptableFileTypes(type) { - if (type === CONST.ATTACHMENT_PICKER_TYPE.IMAGE) { - return 'image/*'; + if (type !== CONST.ATTACHMENT_PICKER_TYPE.IMAGE) { + return; } + + return 'image/*'; } /** diff --git a/src/components/AttachmentPicker/index.native.js b/src/components/AttachmentPicker/index.native.js index 56a4befb6491..20996c420a54 100644 --- a/src/components/AttachmentPicker/index.native.js +++ b/src/components/AttachmentPicker/index.native.js @@ -120,14 +120,17 @@ class AttachmentPicker extends Component { * @param {ImagePickerResponse|DocumentPickerResponse} attachment */ pickAttachment(attachment) { - if (attachment) { - if (attachment.width === -1 || attachment.height === -1) { - this.showImageCorruptionAlert(); - return; - } - const result = getDataForUpload(attachment); - this.completeAttachmentSelection(result); + if (!attachment) { + return; + } + + if (attachment.width === -1 || attachment.height === -1) { + this.showImageCorruptionAlert(); + return; } + + const result = getDataForUpload(attachment); + this.completeAttachmentSelection(result); } /** @@ -211,10 +214,12 @@ class AttachmentPicker extends Component { */ showDocumentPicker() { return RNDocumentPicker.pick(documentPickerOptions).catch((error) => { - if (!RNDocumentPicker.isCancel(error)) { - this.showGeneralAlert(error.message); - throw error; + if (RNDocumentPicker.isCancel(error)) { + return; } + + this.showGeneralAlert(error.message); + throw error; }); } @@ -222,9 +227,11 @@ class AttachmentPicker extends Component { * Triggers the `onPicked` callback with the selected attachment */ completeAttachmentSelection() { - if (this.state.result) { - this.state.onPicked(this.state.result); + if (!this.state.result) { + return; } + + this.state.onPicked(this.state.result); } /** diff --git a/src/components/AvatarWithImagePicker.js b/src/components/AvatarWithImagePicker.js index 9bbdd7064e3e..0243be64389c 100644 --- a/src/components/AvatarWithImagePicker.js +++ b/src/components/AvatarWithImagePicker.js @@ -82,9 +82,11 @@ class AvatarWithImagePicker extends React.Component { } componentDidMount() { - if (this.props.isUploading) { - this.animation.start(); + if (!this.props.isUploading) { + return; } + + this.animation.start(); } componentDidUpdate(prevProps) { diff --git a/src/components/AvatarWithIndicator.js b/src/components/AvatarWithIndicator.js index 399bd610121c..b109d0d611f2 100644 --- a/src/components/AvatarWithIndicator.js +++ b/src/components/AvatarWithIndicator.js @@ -46,9 +46,11 @@ class AvatarWithIndicator extends PureComponent { } componentDidMount() { - if (this.props.isSyncing) { - this.animation.start(); + if (!this.props.isSyncing) { + return; } + + this.animation.start(); } componentDidUpdate(prevProps) { diff --git a/src/components/Button.js b/src/components/Button.js index 3038e0c64f21..c62283607481 100644 --- a/src/components/Button.js +++ b/src/components/Button.js @@ -92,9 +92,11 @@ class Button extends Component { // Setup and attach keypress handler for pressing the button with Enter key this.unsubscribe = KeyboardShortcut.subscribe('Enter', () => { - if (!this.props.isDisabled && !this.props.isLoading) { - this.props.onPress(); + if (this.props.isDisabled || this.props.isLoading) { + return; } + + this.props.onPress(); }, [], true); } diff --git a/src/components/CheckboxWithTooltip/CheckboxWithTooltipForMobileWebAndNative.js b/src/components/CheckboxWithTooltip/CheckboxWithTooltipForMobileWebAndNative.js index 9b6edab11c49..c99cf59d9017 100644 --- a/src/components/CheckboxWithTooltip/CheckboxWithTooltipForMobileWebAndNative.js +++ b/src/components/CheckboxWithTooltip/CheckboxWithTooltipForMobileWebAndNative.js @@ -12,9 +12,11 @@ class CheckboxWithTooltipForMobileWebAndNative extends React.Component { } componentDidUpdate() { - if (this.props.toggleTooltip) { - Growl.show(this.props.text, this.props.growlType, 3000); + if (!this.props.toggleTooltip) { + return; } + + Growl.show(this.props.text, this.props.growlType, 3000); } /** diff --git a/src/components/ContextMenuItem.js b/src/components/ContextMenuItem.js index 7c2fb58a48b0..9f92cb181aab 100644 --- a/src/components/ContextMenuItem.js +++ b/src/components/ContextMenuItem.js @@ -47,9 +47,11 @@ class ContextMenuItem extends Component { } componentWillUnmount() { - if (this.successResetTimer) { - clearTimeout(this.successResetTimer); + if (!this.successResetTimer) { + return; } + + clearTimeout(this.successResetTimer); } /** diff --git a/src/components/DatePicker/index.js b/src/components/DatePicker/index.js index 3688fbc6f4a2..d6362d62b82a 100644 --- a/src/components/DatePicker/index.js +++ b/src/components/DatePicker/index.js @@ -55,9 +55,11 @@ class Datepicker extends React.Component { * don't make this very obvious. To avoid confusion we open the datepicker when the user focuses the field */ showDatepicker() { - if (this.inputRef) { - this.inputRef.click(); + if (!this.inputRef) { + return; } + + this.inputRef.click(); } render() { diff --git a/src/components/ExpensiTextInput/BaseExpensiTextInput.js b/src/components/ExpensiTextInput/BaseExpensiTextInput.js index 31f94248d571..b22eaa55b2b2 100644 --- a/src/components/ExpensiTextInput/BaseExpensiTextInput.js +++ b/src/components/ExpensiTextInput/BaseExpensiTextInput.js @@ -43,21 +43,25 @@ class BaseExpensiTextInput extends Component { componentDidMount() { // We are manually managing focus to prevent this issue: https://github.com/Expensify/App/issues/4514 - if (this.props.autoFocus && this.input) { - this.input.focus(); + if (!this.props.autoFocus || !this.input) { + return; } + + this.input.focus(); } componentDidUpdate(prevProps) { // activate or deactivate the label when value is changed programmatically from outside - if (prevProps.value !== this.props.value) { - this.value = this.props.value; - - if (this.props.value) { - this.activateLabel(); - } else if (!this.state.isFocused) { - this.deactivateLabel(); - } + if (prevProps.value === this.props.value) { + return; + } + + this.value = this.props.value; + + if (this.props.value) { + this.activateLabel(); + } else if (!this.state.isFocused) { + this.deactivateLabel(); } } @@ -100,21 +104,25 @@ class BaseExpensiTextInput extends Component { } activateLabel() { - if (this.value.length >= 0 && !this.isLabelActive) { - this.animateLabel( - ACTIVE_LABEL_TRANSLATE_Y, - ACTIVE_LABEL_TRANSLATE_X(this.props.translateX), - ACTIVE_LABEL_SCALE, - ); - this.isLabelActive = true; + if (this.value.length < 0 || this.isLabelActive) { + return; } + + this.animateLabel( + ACTIVE_LABEL_TRANSLATE_Y, + ACTIVE_LABEL_TRANSLATE_X(this.props.translateX), + ACTIVE_LABEL_SCALE, + ); + this.isLabelActive = true; } deactivateLabel() { - if (!this.props.forceActiveLabel && this.value.length === 0) { - this.animateLabel(INACTIVE_LABEL_TRANSLATE_Y, INACTIVE_LABEL_TRANSLATE_X, INACTIVE_LABEL_SCALE); - this.isLabelActive = false; + if (this.props.forceActiveLabel || this.value.length !== 0) { + return; } + + this.animateLabel(INACTIVE_LABEL_TRANSLATE_Y, INACTIVE_LABEL_TRANSLATE_X, INACTIVE_LABEL_SCALE); + this.isLabelActive = false; } animateLabel(translateY, translateX, scale) { diff --git a/src/components/FAB/FAB.js b/src/components/FAB/FAB.js index 17cb9930dd51..ad9e72325de4 100644 --- a/src/components/FAB/FAB.js +++ b/src/components/FAB/FAB.js @@ -23,9 +23,11 @@ class FAB extends PureComponent { } componentDidUpdate(prevProps) { - if (prevProps.isActive !== this.props.isActive) { - this.animateFloatingActionButton(); + if (prevProps.isActive === this.props.isActive) { + return; } + + this.animateFloatingActionButton(); } /** diff --git a/src/components/GrowlNotification/index.js b/src/components/GrowlNotification/index.js index 49675a10e3c6..0c83024f1a35 100644 --- a/src/components/GrowlNotification/index.js +++ b/src/components/GrowlNotification/index.js @@ -81,9 +81,11 @@ class GrowlNotification extends Component { { - if (nativeEvent.state === State.ACTIVE) { - this.fling(INACTIVE_POSITION_Y); + if (nativeEvent.state !== State.ACTIVE) { + return; } + + this.fling(INACTIVE_POSITION_Y); }} > diff --git a/src/components/IOUConfirmationList.js b/src/components/IOUConfirmationList.js index 99229892e808..a9bbdc5d55a0 100755 --- a/src/components/IOUConfirmationList.js +++ b/src/components/IOUConfirmationList.js @@ -165,16 +165,20 @@ class IOUConfirmationList extends Component { componentDidMount() { // Only add the Venmo option if we're sending a payment - if (this.props.iouType === CONST.IOU.IOU_TYPE.SEND) { - this.addVenmoPaymentOptionToMenu(); + if (this.props.iouType !== CONST.IOU.IOU_TYPE.SEND) { + return; } + + this.addVenmoPaymentOptionToMenu(); } componentWillUnmount() { - if (this.checkVenmoAvailabilityPromise) { - this.checkVenmoAvailabilityPromise.cancel(); - this.checkVenmoAvailabilityPromise = null; + if (!this.checkVenmoAvailabilityPromise) { + return; } + + this.checkVenmoAvailabilityPromise.cancel(); + this.checkVenmoAvailabilityPromise = null; } /** @@ -331,24 +335,25 @@ class IOUConfirmationList extends Component { * Adds Venmo, if available, as the second option in the menu of payment options */ addVenmoPaymentOptionToMenu() { - // Add Venmo option - if (this.props.localCurrencyCode === CONST.CURRENCY.USD && this.state.participants[0].phoneNumber && isValidUSPhone(this.state.participants[0].phoneNumber)) { - this.checkVenmoAvailabilityPromise = makeCancellablePromise(isAppInstalled('venmo')); - this.checkVenmoAvailabilityPromise - .promise - .then((isVenmoInstalled) => { - if (!isVenmoInstalled) { - return; - } - - this.setState(prevState => ({ - confirmationButtonOptions: [...prevState.confirmationButtonOptions.slice(0, 1), - {text: this.props.translate('iou.settleVenmo'), icon: Venmo}, - ...prevState.confirmationButtonOptions.slice(1), - ], - })); - }); + if (this.props.localCurrencyCode !== CONST.CURRENCY.USD || !this.state.participants[0].phoneNumber || !isValidUSPhone(this.state.participants[0].phoneNumber)) { + return; } + + this.checkVenmoAvailabilityPromise = makeCancellablePromise(isAppInstalled('venmo')); + this.checkVenmoAvailabilityPromise + .promise + .then((isVenmoInstalled) => { + if (!isVenmoInstalled) { + return; + } + + this.setState(prevState => ({ + confirmationButtonOptions: [...prevState.confirmationButtonOptions.slice(0, 1), + {text: this.props.translate('iou.settleVenmo'), icon: Venmo}, + ...prevState.confirmationButtonOptions.slice(1), + ], + })); + }); } /** diff --git a/src/components/ImageWithSizeCalculation.js b/src/components/ImageWithSizeCalculation.js index 883254ccf2ef..c9dc2acdafb9 100644 --- a/src/components/ImageWithSizeCalculation.js +++ b/src/components/ImageWithSizeCalculation.js @@ -41,10 +41,11 @@ class ImageWithSizeCalculation extends PureComponent { } componentDidUpdate(prevProps) { - // Only calculate image size if the source has changed - if (prevProps.url !== this.props.url) { - this.calculateImageSize(); + if (prevProps.url === this.props.url) { + return; } + + this.calculateImageSize(); } componentWillUnmount() { diff --git a/src/components/LocalePicker.js b/src/components/LocalePicker.js index 6e57f4d47160..bcce87b246f2 100644 --- a/src/components/LocalePicker.js +++ b/src/components/LocalePicker.js @@ -53,9 +53,11 @@ const LocalePicker = ({ { - if (locale !== preferredLocale) { - setLocale(locale); + if (locale === preferredLocale) { + return; } + + setLocale(locale); }} items={_.values(localesToLanguages)} size={size} diff --git a/src/components/Modal/BaseModal.js b/src/components/Modal/BaseModal.js index 72f27a080a8a..3e84837245f8 100644 --- a/src/components/Modal/BaseModal.js +++ b/src/components/Modal/BaseModal.js @@ -29,9 +29,11 @@ class BaseModal extends PureComponent { } componentDidUpdate(prevProps) { - if (prevProps.isVisible !== this.props.isVisible) { - willAlertModalBecomeVisible(this.props.isVisible); + if (prevProps.isVisible === this.props.isVisible) { + return; } + + willAlertModalBecomeVisible(this.props.isVisible); } componentWillUnmount() { diff --git a/src/components/Modal/index.js b/src/components/Modal/index.js index f198dbe10f2b..add8feef0a10 100644 --- a/src/components/Modal/index.js +++ b/src/components/Modal/index.js @@ -10,24 +10,30 @@ class Modal extends Component { } componentDidMount() { - if (this.props.shouldCloseOnOutsideClick) { - document.addEventListener('mousedown', this.closeOnOutsideClick); + if (!this.props.shouldCloseOnOutsideClick) { + return; } + + document.addEventListener('mousedown', this.closeOnOutsideClick); } componentWillUnmount() { - if (this.props.shouldCloseOnOutsideClick) { - document.removeEventListener('mousedown', this.closeOnOutsideClick); + if (!this.props.shouldCloseOnOutsideClick) { + return; } + + document.removeEventListener('mousedown', this.closeOnOutsideClick); } closeOnOutsideClick(event) { - if (this.props.isVisible - && this.baseModalRef - && !this.baseModalRef.contains(event.target) - && this.props.shouldCloseOnOutsideClick) { - this.props.onClose(); + if (!this.props.isVisible + || !this.baseModalRef + || this.baseModalRef.contains(event.target) + || !this.props.shouldCloseOnOutsideClick) { + return; } + + this.props.onClose(); } render() { diff --git a/src/components/Onfido/index.js b/src/components/Onfido/index.js index b97e4d1f6772..0c3d0b885da2 100644 --- a/src/components/Onfido/index.js +++ b/src/components/Onfido/index.js @@ -94,9 +94,11 @@ class Onfido extends React.Component { } componentWillUnmount() { - if (this.onfidoOut) { - this.onfidoOut.tearDown(); + if (!this.onfidoOut) { + return; } + + this.onfidoOut.tearDown(); } render() { diff --git a/src/components/PlaidLink/index.native.js b/src/components/PlaidLink/index.native.js index da6fdf8dcb93..bbc7edbd627c 100644 --- a/src/components/PlaidLink/index.native.js +++ b/src/components/PlaidLink/index.native.js @@ -27,9 +27,11 @@ class PlaidLink extends React.Component { } componentWillUnmount() { - if (this.listener) { - this.listener.remove(); + if (!this.listener) { + return; } + + this.listener.remove(); } /** diff --git a/src/components/ScreenWrapper.js b/src/components/ScreenWrapper.js index 74f3c1cb733d..131c3cc271e9 100644 --- a/src/components/ScreenWrapper.js +++ b/src/components/ScreenWrapper.js @@ -67,9 +67,11 @@ class ScreenWrapper extends React.Component { componentDidMount() { this.unsubscribeEscapeKey = KeyboardShortcut.subscribe('Escape', () => { - if (!this.props.modal.willAlertModalBecomeVisible) { - Navigation.dismissModal(); + if (this.props.modal.willAlertModalBecomeVisible) { + return; } + + Navigation.dismissModal(); }, [], true); this.unsubscribeTransitionEnd = onScreenTransitionEnd(this.props.navigation, () => { diff --git a/src/components/Switch.js b/src/components/Switch.js index f01ed19fdb9a..73ec6ea5986b 100644 --- a/src/components/Switch.js +++ b/src/components/Switch.js @@ -22,9 +22,11 @@ class Switch extends Component { } componentDidUpdate(prevProps) { - if (prevProps.isOn !== this.props.isOn) { - this.toggleSwitch(); + if (prevProps.isOn === this.props.isOn) { + return; } + + this.toggleSwitch(); } toggleSwitch() { diff --git a/src/components/TextInputFocusable/index.android.js b/src/components/TextInputFocusable/index.android.js index 3e520adceaee..1c966d74ab03 100644 --- a/src/components/TextInputFocusable/index.android.js +++ b/src/components/TextInputFocusable/index.android.js @@ -45,16 +45,20 @@ class TextInputFocusable extends React.Component { // get a ref to the inner textInput element e.g. if we do // this.textInput = el} /> this will not // return a ref to the component, but rather the HTML element by default - if (this.props.forwardedRef && _.isFunction(this.props.forwardedRef)) { - this.props.forwardedRef(this.textInput); + if (!this.props.forwardedRef || !_.isFunction(this.props.forwardedRef)) { + return; } + + this.props.forwardedRef(this.textInput); } componentDidUpdate(prevProps) { - if (!prevProps.shouldClear && this.props.shouldClear) { - this.textInput.clear(); - this.props.onClear(); + if (prevProps.shouldClear || !this.props.shouldClear) { + return; } + + this.textInput.clear(); + this.props.onClear(); } render() { diff --git a/src/components/TextInputFocusable/index.ios.js b/src/components/TextInputFocusable/index.ios.js index 24c9779a1530..42b5fff111af 100644 --- a/src/components/TextInputFocusable/index.ios.js +++ b/src/components/TextInputFocusable/index.ios.js @@ -55,16 +55,20 @@ class TextInputFocusable extends React.Component { // get a ref to the inner textInput element e.g. if we do // this.textInput = el} /> this will not // return a ref to the component, but rather the HTML element by default - if (this.props.forwardedRef && _.isFunction(this.props.forwardedRef)) { - this.props.forwardedRef(this.textInput); + if (!this.props.forwardedRef || !_.isFunction(this.props.forwardedRef)) { + return; } + + this.props.forwardedRef(this.textInput); } componentDidUpdate(prevProps) { - if (!prevProps.shouldClear && this.props.shouldClear) { - this.textInput.clear(); - this.props.onClear(); + if (prevProps.shouldClear || !this.props.shouldClear) { + return; } + + this.textInput.clear(); + this.props.onClear(); } render() { diff --git a/src/components/TextInputFocusable/index.js b/src/components/TextInputFocusable/index.js index 44f386ef3d64..95fbefce1ceb 100755 --- a/src/components/TextInputFocusable/index.js +++ b/src/components/TextInputFocusable/index.js @@ -164,14 +164,16 @@ class TextInputFocusable extends React.Component { } componentWillUnmount() { - if (this.textInput) { - document.removeEventListener('dragover', this.dragNDropListener); - document.removeEventListener('dragenter', this.dragNDropListener); - document.removeEventListener('dragleave', this.dragNDropListener); - document.removeEventListener('drop', this.dragNDropListener); - this.textInput.removeEventListener('paste', this.handlePaste); - this.textInput.removeEventListener('wheel', this.handleWheel); + if (!this.textInput) { + return; } + + document.removeEventListener('dragover', this.dragNDropListener); + document.removeEventListener('dragenter', this.dragNDropListener); + document.removeEventListener('dragleave', this.dragNDropListener); + document.removeEventListener('drop', this.dragNDropListener); + this.textInput.removeEventListener('paste', this.handlePaste); + this.textInput.removeEventListener('wheel', this.handleWheel); } /** @@ -312,11 +314,13 @@ class TextInputFocusable extends React.Component { * @param {Object} event native Event */ handleWheel(event) { - if (event.target === document.activeElement) { - this.textInput.scrollTop += event.deltaY; - event.preventDefault(); - event.stopPropagation(); + if (event.target !== document.activeElement) { + return; } + + this.textInput.scrollTop += event.deltaY; + event.preventDefault(); + event.stopPropagation(); } /** diff --git a/src/components/Tooltip/index.js b/src/components/Tooltip/index.js index 428c4dd347f5..57b1c5044597 100644 --- a/src/components/Tooltip/index.js +++ b/src/components/Tooltip/index.js @@ -54,10 +54,12 @@ class Tooltip extends PureComponent { } componentDidUpdate(prevProps) { - if (this.props.windowWidth !== prevProps.windowWidth || this.props.windowHeight !== prevProps.windowHeight) { - this.getWrapperPosition() - .then(({x, y}) => this.setStateIfMounted({xOffset: x, yOffset: y})); + if (this.props.windowWidth === prevProps.windowWidth && this.props.windowHeight === prevProps.windowHeight) { + return; } + + this.getWrapperPosition() + .then(({x, y}) => this.setStateIfMounted({xOffset: x, yOffset: y})); } componentWillUnmount() { @@ -72,9 +74,11 @@ class Tooltip extends PureComponent { * @param {Object} newState */ setStateIfMounted(newState) { - if (this.isComponentMounted) { - this.setState(newState); + if (!this.isComponentMounted) { + return; } + + this.setState(newState); } /** diff --git a/src/components/VideoChatButtonAndMenu.js b/src/components/VideoChatButtonAndMenu.js index 366505f39b4a..bfe16c3abfcc 100755 --- a/src/components/VideoChatButtonAndMenu.js +++ b/src/components/VideoChatButtonAndMenu.js @@ -83,11 +83,13 @@ class VideoChatButtonAndMenu extends Component { * This gets called onLayout to find the cooridnates of the wrapper for the video chat button. */ measureVideoChatIconPosition() { - if (this.videoChatIconWrapper) { - this.videoChatIconWrapper.measureInWindow((x, y) => this.setState({ - videoChatIconPosition: {x, y}, - })); + if (!this.videoChatIconWrapper) { + return; } + + this.videoChatIconWrapper.measureInWindow((x, y) => this.setState({ + videoChatIconPosition: {x, y}, + })); } render() { diff --git a/src/libs/API.js b/src/libs/API.js index f7a51a777900..f5635fa7fc5c 100644 --- a/src/libs/API.js +++ b/src/libs/API.js @@ -1,14 +1,18 @@ import _ from 'underscore'; +import lodashGet from 'lodash/get'; import Onyx from 'react-native-onyx'; import CONST from '../CONST'; import CONFIG from '../CONFIG'; import ONYXKEYS from '../ONYXKEYS'; +// eslint-disable-next-line import/no-cycle import redirectToSignIn from './actions/SignInRedirect'; import * as Network from './Network'; import isViaExpensifyCashNative from './isViaExpensifyCashNative'; // eslint-disable-next-line import/no-cycle import LogUtil from './Log'; +// eslint-disable-next-line import/no-cycle +import * as Session from './actions/Session'; let isAuthenticating; let credentials; @@ -91,21 +95,23 @@ Network.registerParameterEnhancer(addDefaultValuesToParameters); */ function requireParameters(parameterNames, parameters, commandName) { parameterNames.forEach((parameterName) => { - if (!_(parameters).has(parameterName) - || parameters[parameterName] === null - || parameters[parameterName] === undefined + if (_(parameters).has(parameterName) + && parameters[parameterName] !== null + && parameters[parameterName] !== undefined ) { - const propertiesToRedact = ['authToken', 'password', 'partnerUserSecret', 'twoFactorAuthCode']; - const parametersCopy = _.chain(parameters) - .clone() - .mapObject((val, key) => (_.contains(propertiesToRedact, key) ? '' : val)) - .value(); - const keys = _(parametersCopy).keys().join(', ') || 'none'; - - let error = `Parameter ${parameterName} is required for "${commandName}". `; - error += `Supplied parameters: ${keys}`; - throw new Error(error); + return; } + + const propertiesToRedact = ['authToken', 'password', 'partnerUserSecret', 'twoFactorAuthCode']; + const parametersCopy = _.chain(parameters) + .clone() + .mapObject((val, key) => (_.contains(propertiesToRedact, key) ? '' : val)) + .value(); + const keys = _(parametersCopy).keys().join(', ') || 'none'; + + let error = `Parameter ${parameterName} is required for "${commandName}". `; + error += `Supplied parameters: ${keys}`; + throw new Error(error); }); } @@ -153,7 +159,8 @@ Network.registerResponseHandler((queuedRequest, response) => { // There are some API requests that should not be retried when there is an auth failure like // creating and deleting logins. In those cases, they should handle the original response instead // of the new response created by handleExpiredAuthToken. - if (queuedRequest.data.doNotRetry || unableToReauthenticate) { + const shouldRetry = lodashGet(queuedRequest, 'data.shouldRetry'); + if (!shouldRetry || unableToReauthenticate) { queuedRequest.resolve(response); return; } @@ -182,7 +189,7 @@ Network.registerErrorHandler((queuedRequest, error) => { } // Set an error state and signify we are done loading - Onyx.merge(ONYXKEYS.SESSION, {loading: false, error: 'Cannot connect to server'}); + Session.setSessionLoadingAndError(false, 'Cannot connect to server'); // Reject the queued request with an API offline error so that the original caller can handle it. queuedRequest.reject(new Error(CONST.ERROR.API_OFFLINE)); @@ -221,7 +228,7 @@ function Authenticate(parameters) { partnerUserSecret: parameters.partnerUserSecret, twoFactorAuthCode: parameters.twoFactorAuthCode, authToken: parameters.authToken, - doNotRetry: true, + shouldRetry: false, // Force this request to be made because the network queue is paused when re-authentication is happening forceNetworkRequest: true, @@ -283,10 +290,7 @@ function reauthenticate(command = '') { // Update authToken in Onyx and in our local variables so that API requests will use the // new authToken - Onyx.merge(ONYXKEYS.SESSION, { - authToken: response.authToken, - encryptedAuthToken: response.encryptedAuthToken, - }); + Session.updateSessionAuthTokens(response.authToken, response.encryptedAuthToken); authToken = response.authToken; // The authentication process is finished so the network can be unpaused to continue @@ -340,7 +344,7 @@ function AuthenticateWithAccountID(parameters) { accountID: parameters.accountID, validateCode: parameters.validateCode, twoFactorAuthCode: parameters.twoFactorAuthCode, - doNotRetry: true, + shouldRetry: false, }); } @@ -398,7 +402,7 @@ function User_SignUp(parameters) { * @param {String} parameters.partnerPassword * @param {String} parameters.partnerUserID * @param {String} parameters.partnerUserSecret - * @param {Boolean} [parameters.doNotRetry] + * @param {Boolean} [parameters.shouldRetry] * @param {String} [parameters.email] * @returns {Promise} */ @@ -419,12 +423,12 @@ function CreateLogin(parameters) { * @param {String} parameters.partnerUserID * @param {String} parameters.partnerName * @param {String} parameters.partnerPassword - * @param {Boolean} parameters.doNotRetry + * @param {Boolean} parameters.shouldRetry * @returns {Promise} */ function DeleteLogin(parameters) { const commandName = 'DeleteLogin'; - requireParameters(['partnerUserID', 'partnerName', 'partnerPassword', 'doNotRetry'], + requireParameters(['partnerUserID', 'partnerName', 'partnerPassword', 'shouldRetry'], parameters, commandName); return Network.post(commandName, parameters); } diff --git a/src/libs/ActiveClientManager/index.js b/src/libs/ActiveClientManager/index.js index 3b23f22c83c1..e0bc5fc5f371 100644 --- a/src/libs/ActiveClientManager/index.js +++ b/src/libs/ActiveClientManager/index.js @@ -2,6 +2,7 @@ import _ from 'underscore'; import Onyx from 'react-native-onyx'; import Str from 'expensify-common/lib/str'; import ONYXKEYS from '../../ONYXKEYS'; +import * as ActiveClients from '../actions/ActiveClients'; const clientID = Str.guid(); const maxClients = 20; @@ -21,7 +22,7 @@ Onyx.connect({ activeClients = !val ? [] : val; if (activeClients.length >= maxClients) { activeClients.shift(); - Onyx.set(ONYXKEYS.ACTIVE_CLIENTS, activeClients); + ActiveClients.setActiveClients(activeClients); } }, }); @@ -30,7 +31,8 @@ Onyx.connect({ * Add our client ID to the list of active IDs */ function init() { - Onyx.merge(ONYXKEYS.ACTIVE_CLIENTS, [clientID]).then(isInitialized); + ActiveClients.addClient(clientID) + .then(isInitialized); } function isReady() { diff --git a/src/libs/DateUtils.js b/src/libs/DateUtils.js index a4741d4aa31c..0ee479a6d3bc 100644 --- a/src/libs/DateUtils.js +++ b/src/libs/DateUtils.js @@ -10,6 +10,7 @@ import ONYXKEYS from '../ONYXKEYS'; import CONST from '../CONST'; import {translate} from './translate'; import * as PersonalDetails from './actions/PersonalDetails'; +import * as CurrentDate from './actions/CurrentDate'; let timezone = CONST.DEFAULT_TIME_ZONE; Onyx.connect({ @@ -97,7 +98,7 @@ function timestampToRelative(locale, timestamp) { */ const updateCurrentDate = _.throttle(() => { const currentDate = moment().format('YYYY-MM-DD'); - Onyx.set(ONYXKEYS.CURRENT_DATE, currentDate); + CurrentDate.setCurrentDate(currentDate); }, 1000 * 60 * 60 * 3); // 3 hours /** diff --git a/src/libs/Log.js b/src/libs/Log.js index 14a32f198651..fe48c6ef7f61 100644 --- a/src/libs/Log.js +++ b/src/libs/Log.js @@ -1,3 +1,6 @@ +// Making an exception to this rule here since we don't need an "action" for Log and Log should just be used directly. Creating a Log +// action would likely cause confusion about which one to use. But most other API methods should happen inside an action file. +/* eslint-disable rulesdir/no-api-in-views */ import Logger from 'expensify-common/lib/Logger'; import CONFIG from '../CONFIG'; import getPlatform from './getPlatform'; diff --git a/src/libs/Navigation/AppNavigator/AuthScreens.js b/src/libs/Navigation/AppNavigator/AuthScreens.js index 2bcabef3a173..9f94279e144d 100644 --- a/src/libs/Navigation/AppNavigator/AuthScreens.js +++ b/src/libs/Navigation/AppNavigator/AuthScreens.js @@ -60,7 +60,7 @@ import Timers from '../../Timers'; import LogInWithShortLivedTokenPage from '../../../pages/LogInWithShortLivedTokenPage'; import ValidateLoginPage from '../../../pages/ValidateLoginPage'; import defaultScreenOptions from './defaultScreenOptions'; -import * as API from '../../API'; +import * as App from '../../actions/App'; import {cleanupSession} from '../../actions/Session'; Onyx.connect({ @@ -82,12 +82,6 @@ Onyx.connect({ }, }); -let currentPreferredLocale; -Onyx.connect({ - key: ONYXKEYS.NVP_PREFERRED_LOCALE, - callback: val => currentPreferredLocale = val || CONST.DEFAULT_LOCALE, -}); - const RootStack = createCustomModalStackNavigator(); // We want to delay the re-rendering for components(e.g. ReportActionCompose) @@ -140,17 +134,7 @@ class AuthScreens extends React.Component { // Fetch some data we need on initialization NameValuePair.get(CONST.NVP.PRIORITY_MODE, ONYXKEYS.NVP_PRIORITY_MODE, 'default'); NameValuePair.get(CONST.NVP.IS_FIRST_TIME_NEW_EXPENSIFY_USER, ONYXKEYS.NVP_IS_FIRST_TIME_NEW_EXPENSIFY_USER, true); - - API.Get({ - returnValueList: 'nameValuePairs', - nvpNames: ONYXKEYS.NVP_PREFERRED_LOCALE, - }).then((response) => { - const preferredLocale = lodashGet(response, ['nameValuePairs', 'preferredLocale'], CONST.DEFAULT_LOCALE); - if (preferredLocale !== currentPreferredLocale) { - Onyx.set(ONYXKEYS.NVP_PREFERRED_LOCALE, preferredLocale); - } - }); - + App.getLocale(); PersonalDetails.fetchPersonalDetails(); User.getUserDetails(); User.getBetas(); diff --git a/src/libs/Navigation/Navigation.js b/src/libs/Navigation/Navigation.js index 2cf2c03586ac..85bfb3fa2daa 100644 --- a/src/libs/Navigation/Navigation.js +++ b/src/libs/Navigation/Navigation.js @@ -9,6 +9,7 @@ import { } from '@react-navigation/native'; import PropTypes from 'prop-types'; import Onyx from 'react-native-onyx'; +// eslint-disable-next-line import/no-cycle import Log from '../Log'; import linkTo from './linkTo'; import ROUTES from '../../ROUTES'; @@ -30,9 +31,11 @@ const navigationRef = createNavigationContainerRef(); let didTapNotificationBeforeReady = false; function setDidTapNotification() { - if (!navigationRef.isReady()) { - didTapNotificationBeforeReady = true; + if (navigationRef.isReady()) { + return; } + + didTapNotificationBeforeReady = true; } /** diff --git a/src/libs/Network.js b/src/libs/Network.js index aa1fd853ae82..2b5e602869d6 100644 --- a/src/libs/Network.js +++ b/src/libs/Network.js @@ -5,6 +5,7 @@ import HttpUtils from './HttpUtils'; import ONYXKEYS from '../ONYXKEYS'; import * as ActiveClientManager from './ActiveClientManager'; import CONST from '../CONST'; +import * as NetworkRequestQueue from './actions/NetworkRequestQueue'; let isQueuePaused = false; @@ -55,7 +56,7 @@ function processOfflineQueue(persistedRequests) { // Merge the persisted requests with the requests in memory then clear out the queue as we only need to load // this once when the app initializes networkRequestQueue = [...networkRequestQueue, ...persistedRequests]; - Onyx.set(ONYXKEYS.NETWORK_REQUEST_QUEUE, []); + NetworkRequestQueue.clearPersistedRequests(); didLoadPersistedRequests = true; } @@ -121,25 +122,25 @@ function canMakeRequest(request) { * @param {Object} request * @param {String} request.command * @param {Object} request.data - * @param {Boolean} request.data.doNotRetry + * @param {Boolean} request.data.shouldRetry * @param {String} [request.data.returnValueList] * @return {Boolean} */ function canRetryRequest(request) { - const doNotRetry = lodashGet(request, 'data.doNotRetry', false); - const logParams = {command: request.command, doNotRetry, isQueuePaused}; + const shouldRetry = lodashGet(request, 'data.shouldRetry'); + const logParams = {command: request.command, shouldRetry, isQueuePaused}; const returnValueList = lodashGet(request, 'data.returnValueList'); if (returnValueList) { logParams.returnValueList = returnValueList; } - if (doNotRetry) { + if (!shouldRetry) { console.debug('Skipping request that should not be re-tried: ', logParams); } else { console.debug('Skipping request and re-queueing: ', logParams); } - return !doNotRetry; + return shouldRetry; } /** @@ -156,13 +157,14 @@ function processNetworkRequestQueue() { // If we have a request then we need to check if it can be persisted in case we close the tab while offline. // We filter persisted requests from the normal Queue to remove duplicates networkRequestQueue = _.reject(networkRequestQueue, (request) => { - if (!request.data.doNotRetry && request.data.persist) { + const shouldRetry = lodashGet(request, 'data.shouldRetry'); + if (shouldRetry && request.data.persist) { retryableRequests.push(request); return true; } }); if (retryableRequests.length) { - Onyx.merge(ONYXKEYS.NETWORK_REQUEST_QUEUE, retryableRequests); + NetworkRequestQueue.saveRetryableRequests(retryableRequests); } return; } @@ -175,7 +177,7 @@ function processNetworkRequestQueue() { // Some requests should be retried and will end up here if the following conditions are met: // - the queue is paused // - the request does not have forceNetworkRequest === true - // - the request does not have doNotRetry === true + // - the request does not have shouldRetry === false const requestsToProcessOnNextRun = []; _.each(networkRequestQueue, (queuedRequest) => { @@ -216,7 +218,7 @@ function processNetworkRequestQueue() { // As multiple client will be sharing the same Queue and NETWORK_REQUEST_QUEUE is synchronized among clients, // we only ask Leader client to clear the queue if (ActiveClientManager.isClientTheLeader() && didLoadPersistedRequests) { - Onyx.set(ONYXKEYS.NETWORK_REQUEST_QUEUE, []); + NetworkRequestQueue.clearPersistedRequests(); } // User could have bad connectivity and he can go offline multiple times @@ -260,6 +262,11 @@ function post(command, data = {}, type = CONST.NETWORK.METHOD.POST, shouldUseSec shouldUseSecure, }; + // All requests should be retried by default + if (_.isUndefined(request.data.shouldRetry)) { + request.data.shouldRetry = true; + } + // Add the request to a queue of actions to perform networkRequestQueue.push(request); diff --git a/src/libs/NetworkConnection.js b/src/libs/NetworkConnection.js index 4e83f5a6d96c..f8f3d7a0e768 100644 --- a/src/libs/NetworkConnection.js +++ b/src/libs/NetworkConnection.js @@ -1,9 +1,8 @@ import _ from 'underscore'; -import Onyx from 'react-native-onyx'; import NetInfo from './NetInfo'; -import ONYXKEYS from '../ONYXKEYS'; import AppStateMonitor from './AppStateMonitor'; import promiseAllSettled from './promiseAllSettled'; +import * as Network from './actions/Network'; // NetInfo.addEventListener() returns a function used to unsubscribe the // listener so we must create a reference to it and call it in stopListeningForReconnect() @@ -20,9 +19,9 @@ const reconnectionCallbacks = []; */ const triggerReconnectionCallbacks = _.throttle((reason) => { logInfo(`[NetworkConnection] Firing reconnection callbacks because ${reason}`); - Onyx.set(ONYXKEYS.IS_LOADING_AFTER_RECONNECT, true); + Network.setIsLoadingAfterReconnect(true); promiseAllSettled(_.map(reconnectionCallbacks, callback => callback())) - .then(() => Onyx.set(ONYXKEYS.IS_LOADING_AFTER_RECONNECT, false)); + .then(() => Network.setIsLoadingAfterReconnect(false)); }, 5000, {trailing: false}); /** @@ -32,7 +31,7 @@ const triggerReconnectionCallbacks = _.throttle((reason) => { * @param {Boolean} isCurrentlyOffline */ function setOfflineStatus(isCurrentlyOffline) { - Onyx.merge(ONYXKEYS.NETWORK, {isOffline: isCurrentlyOffline}); + Network.setIsOffline(isCurrentlyOffline); // When reconnecting, ie, going from offline to online, all the reconnection callbacks // are triggered (this is usually Actions that need to re-download data from the server) diff --git a/src/libs/Notification/LocalNotification/BrowserNotifications.js b/src/libs/Notification/LocalNotification/BrowserNotifications.js index f9d606b7e857..cf775a56eae9 100644 --- a/src/libs/Notification/LocalNotification/BrowserNotifications.js +++ b/src/libs/Notification/LocalNotification/BrowserNotifications.js @@ -1,10 +1,9 @@ // Web and desktop implementation only. Do not import for direct use. Use LocalNotification. import _ from 'underscore'; import Str from 'expensify-common/lib/str'; -import Onyx from 'react-native-onyx'; import focusApp from './focusApp'; import EXPENSIFY_ICON_URL from '../../../../assets/images/expensify-logo-round-clearspace.png'; -import ONYXKEYS from '../../../ONYXKEYS'; +import * as App from '../../actions/App'; const DEFAULT_DELAY = 4000; @@ -130,7 +129,7 @@ export default { body: 'A new version of this app is available!', delay: 0, onClick: () => { - Onyx.merge(ONYXKEYS.UPDATE_AVAILABLE, true); + App.triggerUpdateAvailable(); }, }); }, diff --git a/src/libs/Notification/PushNotification/index.native.js b/src/libs/Notification/PushNotification/index.native.js index fe03dd3f4a28..c3c5d451e2ff 100644 --- a/src/libs/Notification/PushNotification/index.native.js +++ b/src/libs/Notification/PushNotification/index.native.js @@ -86,9 +86,11 @@ function register(accountID) { // Get permissions to display push notifications (prompts user on iOS, but not Android) UrbanAirship.enableUserPushNotifications() .then((isEnabled) => { - if (!isEnabled) { - Log.info('[PUSH_NOTIFICATIONS] User has disabled visible push notifications for this app.'); + if (isEnabled) { + return; } + + Log.info('[PUSH_NOTIFICATIONS] User has disabled visible push notifications for this app.'); }); // Register this device as a named user in AirshipAPI. diff --git a/src/libs/OptionsListUtils.js b/src/libs/OptionsListUtils.js index 8d25dbb78479..608301af4d4f 100644 --- a/src/libs/OptionsListUtils.js +++ b/src/libs/OptionsListUtils.js @@ -47,9 +47,11 @@ const reportsWithDraft = {}; Onyx.connect({ key: ONYXKEYS.COLLECTION.REPORTS_WITH_DRAFT, callback: (hasDraft, key) => { - if (key) { - reportsWithDraft[key] = hasDraft; + if (!key) { + return; } + + reportsWithDraft[key] = hasDraft; }, }); @@ -57,9 +59,11 @@ const policies = {}; Onyx.connect({ key: ONYXKEYS.COLLECTION.POLICY, callback: (policy, key) => { - if (policy && key && policy.name) { - policies[key] = policy; + if (!policy || !key || !policy.name) { + return; } + + policies[key] = policy; }, }); @@ -67,9 +71,11 @@ const iouReports = {}; Onyx.connect({ key: ONYXKEYS.COLLECTION.REPORT_IOUS, callback: (iouReport, key) => { - if (iouReport && key && iouReport.ownerEmail) { - iouReports[key] = iouReport; + if (!iouReport || !key || !iouReport.ownerEmail) { + return; } + + iouReports[key] = iouReport; }, }); diff --git a/src/libs/Performance.js b/src/libs/Performance.js index 371eb3755d8d..87ce1aeb4dcd 100644 --- a/src/libs/Performance.js +++ b/src/libs/Performance.js @@ -21,13 +21,14 @@ let rnPerformance; function diffObject(object, base) { function changes(obj, comparisonObject) { return lodashTransform(obj, (result, value, key) => { - if (!_.isEqual(value, comparisonObject[key])) { - // eslint-disable-next-line no-param-reassign - result[key] = ( - _.isObject(value) && _.isObject(comparisonObject[key])) - ? changes(value, comparisonObject[key]) - : value; + if (_.isEqual(value, comparisonObject[key])) { + return; } + + // eslint-disable-next-line no-param-reassign + result[key] = (_.isObject(value) && _.isObject(comparisonObject[key])) + ? changes(value, comparisonObject[key]) + : value; }); } return changes(object, base); diff --git a/src/libs/Pusher/pusher.js b/src/libs/Pusher/pusher.js index 88e3c8943400..b4f1a4fa1a42 100644 --- a/src/libs/Pusher/pusher.js +++ b/src/libs/Pusher/pusher.js @@ -1,6 +1,7 @@ import _ from 'underscore'; import Pusher from './library'; import TYPE from './EventType'; +// eslint-disable-next-line import/no-cycle import Log from '../Log'; let socket; diff --git a/src/libs/PusherConnectionManager.js b/src/libs/PusherConnectionManager.js index 9873ed33d20c..16923e3830d8 100644 --- a/src/libs/PusherConnectionManager.js +++ b/src/libs/PusherConnectionManager.js @@ -1,23 +1,7 @@ -import _ from 'underscore'; import * as Pusher from './Pusher/pusher'; -import * as API from './API'; +import * as Session from './actions/Session'; import Log from './Log'; -// It's necessary to throttle requests to reauthenticate since calling this multiple times will cause Pusher to -// reconnect each time when we only need to reconnect once. This way, if an authToken is expired and we try to -// subscribe to a bunch of channels at once we will only reauthenticate and force reconnect Pusher once. -const reauthenticate = _.throttle(() => { - Log.info('[Pusher] Re-authenticating and then reconnecting'); - API.reauthenticate('Push_Authenticate') - .then(() => Pusher.reconnect()) - .catch(() => { - console.debug( - '[PusherConnectionManager]', - 'Unable to re-authenticate Pusher because we are offline.', - ); - }); -}, 5000, {trailing: false}); - function init() { /** * When authTokens expire they will automatically be refreshed. @@ -27,34 +11,7 @@ function init() { */ Pusher.registerCustomAuthorizer(channel => ({ authorize: (socketID, callback) => { - Log.info('[PusherConnectionManager] Attempting to authorize Pusher', false, {channelName: channel.name}); - - API.Push_Authenticate({ - socket_id: socketID, - channel_name: channel.name, - doNotRetry: true, - forceNetworkRequest: true, - }) - .then((data) => { - if (data.jsonCode === 407) { - callback(new Error('Expensify session expired'), {auth: ''}); - - // Attempt to refresh the authToken then reconnect to Pusher - reauthenticate(); - return; - } - - Log.info( - '[PusherConnectionManager] Pusher authenticated successfully', - false, - {channelName: channel.name}, - ); - callback(null, data); - }) - .catch((error) => { - Log.info('[PusherConnectionManager] Unhandled error: ', false, {channelName: channel.name}); - callback(error, {auth: ''}); - }); + Session.authenticatePusher(socketID, channel.name, callback); }, })); @@ -68,7 +25,7 @@ function init() { switch (eventName) { case 'error': Log.info('[PusherConnectionManager] error event', false, {error: data}); - reauthenticate(); + Session.reauthenticatePusher(); break; case 'connected': Log.info('[PusherConnectionManager] connected event'); diff --git a/src/libs/ReportActionComposeFocusManager.js b/src/libs/ReportActionComposeFocusManager.js index 02e1a878a116..2acbfadf98a8 100644 --- a/src/libs/ReportActionComposeFocusManager.js +++ b/src/libs/ReportActionComposeFocusManager.js @@ -19,9 +19,11 @@ function onComposerFocus(callback) { * */ function focus() { - if (_.isFunction(focusCallback)) { - focusCallback(); + if (!_.isFunction(focusCallback)) { + return; } + + focusCallback(); } /** diff --git a/src/libs/ReportScrollManager/index.js b/src/libs/ReportScrollManager/index.js index de784db48bf1..748346f8e5ce 100644 --- a/src/libs/ReportScrollManager/index.js +++ b/src/libs/ReportScrollManager/index.js @@ -24,9 +24,11 @@ function scrollToIndex(index, isEditing) { * */ function scrollToBottom() { - if (flatListRef.current) { - flatListRef.current.scrollToOffset({animated: false, offset: 0}); + if (!flatListRef.current) { + return; } + + flatListRef.current.scrollToOffset({animated: false, offset: 0}); } export { diff --git a/src/libs/ReportScrollManager/index.native.js b/src/libs/ReportScrollManager/index.native.js index ffcd1aee531a..27f2feab1434 100644 --- a/src/libs/ReportScrollManager/index.native.js +++ b/src/libs/ReportScrollManager/index.native.js @@ -18,9 +18,11 @@ function scrollToIndex(index) { * */ function scrollToBottom() { - if (flatListRef.current) { - flatListRef.current.scrollToOffset({animated: false, offset: 0}); + if (!flatListRef.current) { + return; } + + flatListRef.current.scrollToOffset({animated: false, offset: 0}); } export { diff --git a/src/libs/SignoutManager.js b/src/libs/SignoutManager.js index 065d8249cd10..9588a3f969b4 100644 --- a/src/libs/SignoutManager.js +++ b/src/libs/SignoutManager.js @@ -1,5 +1,7 @@ import Onyx from 'react-native-onyx'; import ONYXKEYS from '../ONYXKEYS'; +// eslint-disable-next-line import/no-cycle +import * as Session from './actions/Session'; let signoutCallback = () => {}; let errorMessage = ''; @@ -10,7 +12,7 @@ Onyx.connect({ if (!shouldSignOut && val) { signoutCallback(errorMessage); errorMessage = ''; - Onyx.set(ONYXKEYS.SHOULD_SIGN_OUT, false); + Session.setShouldSignOut(false); } shouldSignOut = val; @@ -29,7 +31,7 @@ function registerSignoutCallback(callback) { */ function signOut(message) { errorMessage = message; - Onyx.set(ONYXKEYS.SHOULD_SIGN_OUT, true); + Session.setShouldSignOut(true); } export default { diff --git a/src/libs/actions/ActiveClients.js b/src/libs/actions/ActiveClients.js new file mode 100644 index 000000000000..2a3689b2c099 --- /dev/null +++ b/src/libs/actions/ActiveClients.js @@ -0,0 +1,22 @@ +import Onyx from 'react-native-onyx'; +import ONYXKEYS from '../../ONYXKEYS'; + +/** + * @param {Array} activeClients + */ +function setActiveClients(activeClients) { + Onyx.set(ONYXKEYS.ACTIVE_CLIENTS, activeClients); +} + +/** + * @param {Number} clientID + * @returns {Promise} + */ +function addClient(clientID) { + return Onyx.merge(ONYXKEYS.ACTIVE_CLIENTS, [clientID]); +} + +export { + setActiveClients, + addClient, +}; diff --git a/src/libs/actions/App.js b/src/libs/actions/App.js index dc782eb11651..422f2b6881c6 100644 --- a/src/libs/actions/App.js +++ b/src/libs/actions/App.js @@ -23,6 +23,12 @@ Onyx.connect({ initWithStoredValues: false, }); +let currentPreferredLocale; +Onyx.connect({ + key: ONYXKEYS.NVP_PREFERRED_LOCALE, + callback: val => currentPreferredLocale = val || CONST.DEFAULT_LOCALE, +}); + /** * @param {String} url */ @@ -40,6 +46,20 @@ function setLocale(locale) { Onyx.merge(ONYXKEYS.NVP_PREFERRED_LOCALE, locale); } +function getLocale() { + API.Get({ + returnValueList: 'nameValuePairs', + nvpNames: ONYXKEYS.NVP_PREFERRED_LOCALE, + }).then((response) => { + const preferredLocale = lodashGet(response, ['nameValuePairs', 'preferredLocale'], CONST.DEFAULT_LOCALE); + if (preferredLocale === currentPreferredLocale) { + return; + } + + Onyx.set(ONYXKEYS.NVP_PREFERRED_LOCALE, preferredLocale); + }); +} + function setSidebarLoaded() { if (isSidebarLoaded) { return; @@ -59,8 +79,14 @@ AppState.addEventListener('change', (nextAppState) => { appState = nextAppState; }); +function triggerUpdateAvailable() { + Onyx.merge(ONYXKEYS.UPDATE_AVAILABLE, true); +} + export { setCurrentURL, setLocale, setSidebarLoaded, + getLocale, + triggerUpdateAvailable, }; diff --git a/src/libs/actions/CurrentDate.js b/src/libs/actions/CurrentDate.js new file mode 100644 index 000000000000..078b35a760cb --- /dev/null +++ b/src/libs/actions/CurrentDate.js @@ -0,0 +1,14 @@ +import Onyx from 'react-native-onyx'; +import ONYXKEYS from '../../ONYXKEYS'; + +/** + * @param {String} currentDate + */ +function setCurrentDate(currentDate) { + Onyx.set(ONYXKEYS.CURRENT_DATE, currentDate); +} + +export { + // eslint-disable-next-line import/prefer-default-export + setCurrentDate, +}; diff --git a/src/libs/actions/Link.js b/src/libs/actions/Link.js index 2ccd29510e0c..8623ca0f1573 100644 --- a/src/libs/actions/Link.js +++ b/src/libs/actions/Link.js @@ -35,20 +35,26 @@ function showGrowlIfOffline() { * @param {String} url */ function openOldDotLink(url) { - if (!showGrowlIfOffline()) { - // eslint-disable-next-line max-len - const buildOldDotURL = ({shortLivedAuthToken}) => `${CONFIG.EXPENSIFY.URL_EXPENSIFY_COM}${url}${url.indexOf('?') === -1 ? '?' : '&'}authToken=${shortLivedAuthToken}&email=${encodeURIComponent(currentUserEmail)}`; - asyncOpenURL(API.GetShortLivedAuthToken(), buildOldDotURL); + if (showGrowlIfOffline()) { + return; } + + function buildOldDotURL({shortLivedAuthToken}) { + return `${CONFIG.EXPENSIFY.URL_EXPENSIFY_COM}${url}${url.indexOf('?') === -1 ? '?' : '&'}authToken=${shortLivedAuthToken}&email=${encodeURIComponent(currentUserEmail)}`; + } + + asyncOpenURL(API.GetShortLivedAuthToken(), buildOldDotURL); } /** * @param {String} url */ function openExternalLink(url) { - if (!showGrowlIfOffline()) { - Linking.openURL(url); + if (showGrowlIfOffline()) { + return; } + + Linking.openURL(url); } export { diff --git a/src/libs/actions/NameValuePair.js b/src/libs/actions/NameValuePair.js index 2b0740558311..b62b217199e1 100644 --- a/src/libs/actions/NameValuePair.js +++ b/src/libs/actions/NameValuePair.js @@ -1,6 +1,7 @@ import _ from 'underscore'; import Onyx from 'react-native-onyx'; import lodashGet from 'lodash/get'; +// eslint-disable-next-line import/no-cycle import * as API from '../API'; /** diff --git a/src/libs/actions/Network.js b/src/libs/actions/Network.js new file mode 100644 index 000000000000..12a0709b62a0 --- /dev/null +++ b/src/libs/actions/Network.js @@ -0,0 +1,21 @@ +import Onyx from 'react-native-onyx'; +import ONYXKEYS from '../../ONYXKEYS'; + +/** + * @param {Boolean} isLoadingAfterReconnect + */ +function setIsLoadingAfterReconnect(isLoadingAfterReconnect) { + Onyx.set(ONYXKEYS.IS_LOADING_AFTER_RECONNECT, isLoadingAfterReconnect); +} + +/** + * @param {Boolean} isOffline + */ +function setIsOffline(isOffline) { + Onyx.merge(ONYXKEYS.NETWORK, {isOffline}); +} + +export { + setIsLoadingAfterReconnect, + setIsOffline, +}; diff --git a/src/libs/actions/NetworkRequestQueue.js b/src/libs/actions/NetworkRequestQueue.js new file mode 100644 index 000000000000..d79efb4adef3 --- /dev/null +++ b/src/libs/actions/NetworkRequestQueue.js @@ -0,0 +1,15 @@ +import Onyx from 'react-native-onyx'; +import ONYXKEYS from '../../ONYXKEYS'; + +function clearPersistedRequests() { + Onyx.set(ONYXKEYS.NETWORK_REQUEST_QUEUE, []); +} + +function saveRetryableRequests(retryableRequests) { + Onyx.merge(ONYXKEYS.NETWORK_REQUEST_QUEUE, retryableRequests); +} + +export { + clearPersistedRequests, + saveRetryableRequests, +}; diff --git a/src/libs/actions/PersonalDetails.js b/src/libs/actions/PersonalDetails.js index d46e2b7c52b3..f5fc4dbd3df4 100644 --- a/src/libs/actions/PersonalDetails.js +++ b/src/libs/actions/PersonalDetails.js @@ -184,22 +184,24 @@ function getFromReportParticipants(reports) { // skip over default rooms which aren't named by participants. const reportsToUpdate = {}; _.each(reports, (report) => { - if (report.participants.length > 0 || isDefaultRoom(report)) { - const avatars = getReportIcons(report, details); - const reportName = isDefaultRoom(report) - ? report.reportName - : _.chain(report.participants) - .filter(participant => participant !== currentUserEmail) - .map(participant => lodashGet( - formattedPersonalDetails, - [participant, 'displayName'], - participant, - )) - .value() - .join(', '); - - reportsToUpdate[`${ONYXKEYS.COLLECTION.REPORT}${report.reportID}`] = {icons: avatars, reportName}; + if (report.participants.length <= 0 && !isDefaultRoom(report)) { + return; } + + const avatars = getReportIcons(report, details); + const reportName = isDefaultRoom(report) + ? report.reportName + : _.chain(report.participants) + .filter(participant => participant !== currentUserEmail) + .map(participant => lodashGet( + formattedPersonalDetails, + [participant, 'displayName'], + participant, + )) + .value() + .join(', '); + + reportsToUpdate[`${ONYXKEYS.COLLECTION.REPORT}${report.reportID}`] = {icons: avatars, reportName}; }); // We use mergeCollection such that it updates ONYXKEYS.COLLECTION.REPORT in one go. diff --git a/src/libs/actions/Policy.js b/src/libs/actions/Policy.js index bf0c982965e7..23da7c531ab2 100644 --- a/src/libs/actions/Policy.js +++ b/src/libs/actions/Policy.js @@ -15,9 +15,11 @@ const allPolicies = {}; Onyx.connect({ key: ONYXKEYS.COLLECTION.POLICY, callback: (val, key) => { - if (val && key) { - allPolicies[key] = {...allPolicies[key], ...val}; + if (!val || !key) { + return; } + + allPolicies[key] = {...allPolicies[key], ...val}; }, }); @@ -146,15 +148,17 @@ function createAndNavigate(name = '') { function getPolicyList() { API.GetPolicySummaryList() .then((data) => { - if (data.jsonCode === 200) { - const policyCollection = _.reduce(data.policySummaryList, (memo, policy) => ({ - ...memo, - [`${ONYXKEYS.COLLECTION.POLICY}${policy.id}`]: getSimplifiedPolicyObject(policy), - }), {}); - - if (!_.isEmpty(policyCollection)) { - updateAllPolicies(policyCollection); - } + if (data.jsonCode !== 200) { + return; + } + + const policyCollection = _.reduce(data.policySummaryList, (memo, policy) => ({ + ...memo, + [`${ONYXKEYS.COLLECTION.POLICY}${policy.id}`]: getSimplifiedPolicyObject(policy), + }), {}); + + if (!_.isEmpty(policyCollection)) { + updateAllPolicies(policyCollection); } }); } @@ -175,12 +179,16 @@ function createAndGetPolicyList() { function loadFullPolicy(policyID) { API.GetFullPolicy(policyID) .then((data) => { - if (data.jsonCode === 200) { - const policy = lodashGet(data, 'policyList[0]', {}); - if (policy.id) { - Onyx.merge(`${ONYXKEYS.COLLECTION.POLICY}${policy.id}`, getSimplifiedPolicyObject(policy)); - } + if (data.jsonCode !== 200) { + return; + } + + const policy = lodashGet(data, 'policyList[0]', {}); + if (!policy.id) { + return; } + + Onyx.merge(`${ONYXKEYS.COLLECTION.POLICY}${policy.id}`, getSimplifiedPolicyObject(policy)); }); } diff --git a/src/libs/actions/Report.js b/src/libs/actions/Report.js index 378196515301..39434b5ab3fc 100644 --- a/src/libs/actions/Report.js +++ b/src/libs/actions/Report.js @@ -33,10 +33,12 @@ Onyx.connect({ key: ONYXKEYS.SESSION, callback: (val) => { // When signed out, val is undefined - if (val) { - currentUserEmail = val.email; - currentUserAccountID = val.accountID; + if (!val) { + return; } + + currentUserEmail = val.email; + currentUserAccountID = val.accountID; }, }); @@ -382,10 +384,12 @@ function fetchChatReportsByIDs(chatList, shouldRedirectIfInaccessible = false) { return fetchedReports; }) .catch((err) => { - if (err.message === CONST.REPORT.ERROR.INACCESSIBLE_REPORT) { - // eslint-disable-next-line no-use-before-define - handleInaccessibleReport(); + if (err.message !== CONST.REPORT.ERROR.INACCESSIBLE_REPORT) { + return; } + + // eslint-disable-next-line no-use-before-define + handleInaccessibleReport(); }); } @@ -1145,15 +1149,17 @@ function deleteReportComment(reportID, reportAction) { sequenceNumber, }) .then((response) => { - if (response.jsonCode !== 200) { - // Reverse Optimistic Response - reportActionsToMerge[sequenceNumber] = { - ...reportAction, - message: oldMessage, - }; - - Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`, reportActionsToMerge); + if (response.jsonCode === 200) { + return; } + + // Reverse Optimistic Response + reportActionsToMerge[sequenceNumber] = { + ...reportAction, + message: oldMessage, + }; + + Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`, reportActionsToMerge); }); } diff --git a/src/libs/actions/Session.js b/src/libs/actions/Session.js index 9ad4516d01b0..b0d40358dd6a 100644 --- a/src/libs/actions/Session.js +++ b/src/libs/actions/Session.js @@ -1,3 +1,4 @@ +/* eslint-disable import/no-cycle */ import Onyx from 'react-native-onyx'; import Str from 'expensify-common/lib/str'; import _ from 'underscore'; @@ -55,14 +56,16 @@ function createAccount(login) { API.User_SignUp({ email: login, }).then((response) => { - if (response.jsonCode !== 200) { - let errorMessage = response.message || `Unknown API Error: ${response.jsonCode}`; - if (!response.message && response.jsonCode === 405) { - errorMessage = 'Cannot create an account that is under a controlled domain'; - } - Onyx.merge(ONYXKEYS.SESSION, {error: errorMessage}); - Onyx.merge(ONYXKEYS.CREDENTIALS, {login: null}); + if (response.jsonCode === 200) { + return; } + + let errorMessage = response.message || `Unknown API Error: ${response.jsonCode}`; + if (!response.message && response.jsonCode === 405) { + errorMessage = 'Cannot create an account that is under a controlled domain'; + } + Onyx.merge(ONYXKEYS.SESSION, {error: errorMessage}); + Onyx.merge(ONYXKEYS.CREDENTIALS, {login: null}); }); } @@ -77,7 +80,7 @@ function signOut() { partnerUserID: credentials.autoGeneratedLogin, partnerName: CONFIG.EXPENSIFY.PARTNER_NAME, partnerPassword: CONFIG.EXPENSIFY.PARTNER_PASSWORD, - doNotRetry: true, + shouldRetry: false, }) .catch(error => Onyx.merge(ONYXKEYS.SESSION, {error: error.message})); } @@ -178,7 +181,7 @@ function createTemporaryLogin(authToken, email) { partnerPassword: CONFIG.EXPENSIFY.PARTNER_PASSWORD, partnerUserID: autoGeneratedLogin, partnerUserSecret: autoGeneratedPassword, - doNotRetry: true, + shouldRetry: false, forceNetworkRequest: true, email, includeEncryptedAuthToken: true, @@ -197,7 +200,7 @@ function createTemporaryLogin(authToken, email) { partnerUserID: credentials.autoGeneratedLogin, partnerName: CONFIG.EXPENSIFY.PARTNER_NAME, partnerPassword: CONFIG.EXPENSIFY.PARTNER_PASSWORD, - doNotRetry: true, + shouldRetry: false, }) .catch(Log.info); } @@ -310,9 +313,11 @@ function setPassword(password, validateCode, accountID) { Onyx.merge(ONYXKEYS.ACCOUNT, {error: response.message}); }) .catch((response) => { - if (response.title === CONST.PASSWORD_PAGE.ERROR.VALIDATE_CODE_FAILED) { - Onyx.merge(ONYXKEYS.ACCOUNT, {error: translateLocal('setPasswordPage.accountNotValidated')}); + if (response.title !== CONST.PASSWORD_PAGE.ERROR.VALIDATE_CODE_FAILED) { + return; } + + Onyx.merge(ONYXKEYS.ACCOUNT, {error: translateLocal('setPasswordPage.accountNotValidated')}); }) .finally(() => { Onyx.merge(ONYXKEYS.ACCOUNT, {loading: false}); @@ -371,6 +376,22 @@ function clearAccountMessages() { Onyx.merge(ONYXKEYS.ACCOUNT, {error: '', success: ''}); } +/** + * @param {Boolean} loading + * @param {String} error + */ +function setSessionLoadingAndError(loading, error) { + Onyx.merge(ONYXKEYS.SESSION, {loading, error}); +} + +/** + * @param {String} authToken + * @param {String} encryptedAuthToken + */ +function updateSessionAuthTokens(authToken, encryptedAuthToken) { + Onyx.merge(ONYXKEYS.SESSION, {authToken, encryptedAuthToken}); +} + /** * @param {String} authToken * @param {String} password @@ -420,6 +441,71 @@ function validateEmail(accountID, validateCode, password) { }); } +// It's necessary to throttle requests to reauthenticate since calling this multiple times will cause Pusher to +// reconnect each time when we only need to reconnect once. This way, if an authToken is expired and we try to +// subscribe to a bunch of channels at once we will only reauthenticate and force reconnect Pusher once. +const reauthenticatePusher = _.throttle(() => { + Log.info('[Pusher] Re-authenticating and then reconnecting'); + API.reauthenticate('Push_Authenticate') + .then(Pusher.reconnect) + .catch(() => { + console.debug( + '[PusherConnectionManager]', + 'Unable to re-authenticate Pusher because we are offline.', + ); + }); +}, 5000, {trailing: false}); + +/** + * @param {String} socketID + * @param {String} channelName + * @param {Function} callback + */ +function authenticatePusher(socketID, channelName, callback) { + Log.info('[PusherConnectionManager] Attempting to authorize Pusher', false, {channelName}); + + API.Push_Authenticate({ + socket_id: socketID, + channel_name: channelName, + shouldRetry: false, + forceNetworkRequest: true, + }) + .then((data) => { + if (data.jsonCode === 407) { + callback(new Error('Expensify session expired'), {auth: ''}); + + // Attempt to refresh the authToken then reconnect to Pusher + reauthenticatePusher(); + return; + } + + Log.info( + '[PusherConnectionManager] Pusher authenticated successfully', + false, + {channelName}, + ); + callback(null, data); + }) + .catch((error) => { + Log.info('[PusherConnectionManager] Unhandled error: ', false, {channelName}); + callback(error, {auth: ''}); + }); +} + +/** + * @param {Boolean} shouldSignOut + */ +function setShouldSignOut(shouldSignOut) { + Onyx.set(ONYXKEYS.SHOULD_SIGN_OUT, shouldSignOut); +} + +/** + * @param {Boolean} shouldShowComposeInput + */ +function setShouldShowComposeInput(shouldShowComposeInput) { + Onyx.merge(ONYXKEYS.SESSION, {shouldShowComposeInput}); +} + export { continueSessionFromECom, fetchAccountDetails, @@ -433,5 +519,11 @@ export { clearSignInData, cleanupSession, clearAccountMessages, + setSessionLoadingAndError, + updateSessionAuthTokens, validateEmail, + authenticatePusher, + reauthenticatePusher, + setShouldSignOut, + setShouldShowComposeInput, }; diff --git a/src/libs/actions/SignInRedirect.js b/src/libs/actions/SignInRedirect.js index 95e8b862d8bc..9b4b43ee1566 100644 --- a/src/libs/actions/SignInRedirect.js +++ b/src/libs/actions/SignInRedirect.js @@ -1,4 +1,5 @@ import Onyx from 'react-native-onyx'; +// eslint-disable-next-line import/no-cycle import SignoutManager from '../SignoutManager'; import ONYXKEYS from '../../ONYXKEYS'; diff --git a/src/libs/actions/Timing.js b/src/libs/actions/Timing.js index 3a9eba170a89..c7ee5a679c7e 100644 --- a/src/libs/actions/Timing.js +++ b/src/libs/actions/Timing.js @@ -1,4 +1,5 @@ import getPlatform from '../getPlatform'; +// eslint-disable-next-line import/no-cycle import {Graphite_Timer} from '../API'; import {isDevelopment} from '../Environment/Environment'; import Firebase from '../Firebase'; @@ -28,32 +29,34 @@ function start(eventName, shouldUseFirebase = false) { * @param {String} [secondaryName] - optional secondary event name, passed to grafana */ function end(eventName, secondaryName = '') { - if (eventName in timestampData) { - const {startTime, shouldUseFirebase} = timestampData[eventName]; - const eventTime = Date.now() - startTime; + if (!timestampData[eventName]) { + return; + } - if (shouldUseFirebase) { - Firebase.stopTrace(eventName); - } + const {startTime, shouldUseFirebase} = timestampData[eventName]; + const eventTime = Date.now() - startTime; - const grafanaEventName = secondaryName - ? `expensify.cash.${eventName}.${secondaryName}` - : `expensify.cash.${eventName}`; + if (shouldUseFirebase) { + Firebase.stopTrace(eventName); + } - console.debug(`Timing:${grafanaEventName}`, eventTime); - delete timestampData[eventName]; + const grafanaEventName = secondaryName + ? `expensify.cash.${eventName}.${secondaryName}` + : `expensify.cash.${eventName}`; - if (isDevelopment()) { - // Don't create traces on dev as this will mess up the accuracy of data in release builds of the app - return; - } + console.debug(`Timing:${grafanaEventName}`, eventTime); + delete timestampData[eventName]; - Graphite_Timer({ - name: grafanaEventName, - value: eventTime, - platform: `${getPlatform()}`, - }); + if (isDevelopment()) { + // Don't create traces on dev as this will mess up the accuracy of data in release builds of the app + return; } + + Graphite_Timer({ + name: grafanaEventName, + value: eventTime, + platform: `${getPlatform()}`, + }); } /** diff --git a/src/libs/actions/User.js b/src/libs/actions/User.js index 17f8aced5946..4a5d60e4badd 100644 --- a/src/libs/actions/User.js +++ b/src/libs/actions/User.js @@ -1,3 +1,4 @@ +/* eslint-disable import/no-cycle */ import _ from 'underscore'; import lodashGet from 'lodash/get'; import Onyx from 'react-native-onyx'; @@ -60,9 +61,11 @@ function changePasswordAndNavigate(oldPassword, password) { function getBetas() { API.User_GetBetas().then((response) => { - if (response.jsonCode === 200) { - Onyx.set(ONYXKEYS.BETAS, response.betas); + if (response.jsonCode !== 200) { + return; } + + Onyx.set(ONYXKEYS.BETAS, response.betas); }); } @@ -117,9 +120,11 @@ function setExpensifyNewsStatus(subscribed) { API.UpdateAccount({subscribed}) .then((response) => { - if (response.jsonCode !== 200) { - Onyx.merge(ONYXKEYS.USER, {expensifyNewsStatus: !subscribed}); + if (response.jsonCode === 200) { + return; } + + Onyx.merge(ONYXKEYS.USER, {expensifyNewsStatus: !subscribed}); }) .catch(() => { Onyx.merge(ONYXKEYS.USER, {expensifyNewsStatus: !subscribed}); diff --git a/src/libs/toggleReportActionComposeView.js b/src/libs/toggleReportActionComposeView.js index 3b43305ebafb..db12abc9a041 100644 --- a/src/libs/toggleReportActionComposeView.js +++ b/src/libs/toggleReportActionComposeView.js @@ -1,8 +1,9 @@ -import Onyx from 'react-native-onyx'; -import ONYXKEYS from '../ONYXKEYS'; +import * as Session from './actions/Session'; export default (shouldShowComposeInput, isSmallScreenWidth) => { - if (isSmallScreenWidth) { - Onyx.merge(ONYXKEYS.SESSION, {shouldShowComposeInput}); + if (!isSmallScreenWidth) { + return; } + + Session.setShouldShowComposeInput(shouldShowComposeInput); }; diff --git a/src/libs/translate.js b/src/libs/translate.js index adf411542d4d..a024e7e59444 100644 --- a/src/libs/translate.js +++ b/src/libs/translate.js @@ -1,6 +1,8 @@ +import _ from 'underscore'; import lodashGet from 'lodash/get'; import Str from 'expensify-common/lib/str'; import Onyx from 'react-native-onyx'; +// eslint-disable-next-line import/no-cycle import Log from './Log'; import Config from '../CONFIG'; import translations from '../languages/translations'; @@ -11,9 +13,11 @@ let preferredLocale = CONST.DEFAULT_LOCALE; Onyx.connect({ key: ONYXKEYS.NVP_PREFERRED_LOCALE, callback: (val) => { - if (val) { - preferredLocale = val; + if (!val) { + return; } + + preferredLocale = val; }, }); @@ -57,7 +61,7 @@ function translate(locale = CONST.DEFAULT_LOCALE, phrase, variables = {}) { // Phrase is not found in default language, on production log an alert to server // on development throw an error if (Config.IS_IN_PRODUCTION) { - const phraseString = Array.isArray(phrase) ? phrase.join('.') : phrase; + const phraseString = _.isArray(phrase) ? phrase.join('.') : phrase; Log.alert(`${phraseString} was not found in the en locale`); return phraseString; } diff --git a/src/pages/NotFound.js b/src/pages/NotFound.js deleted file mode 100755 index ff82f566c5d9..000000000000 --- a/src/pages/NotFound.js +++ /dev/null @@ -1,46 +0,0 @@ -import React from 'react'; -import { - View, - Image, - TouchableOpacity, - SafeAreaView, -} from 'react-native'; -import styles from '../styles/styles'; -import logo from '../../assets/images/expensify-logo_reversed.png'; -import Navigation from '../libs/Navigation/Navigation'; -import ROUTES from '../ROUTES'; -import withLocalize, {withLocalizePropTypes} from '../components/withLocalize'; -import Text from '../components/Text'; - -const propTypes = { - ...withLocalizePropTypes, -}; - -const NotFound = ({translations: {translate}}) => ( - <> - - - - - 404 - {translate('notFound.chatYouLookingForCannotBeFound')} - - Navigation.navigate(ROUTES.HOME)} - > - {translate('notFound.getMeOutOfHere')} - - - - -); - -NotFound.propTypes = propTypes; -NotFound.displayName = 'NotFound'; -export default withLocalize(NotFound); diff --git a/src/pages/ReimbursementAccount/BeneficialOwnersStep.js b/src/pages/ReimbursementAccount/BeneficialOwnersStep.js index faf2a67216c1..3319f92d1717 100644 --- a/src/pages/ReimbursementAccount/BeneficialOwnersStep.js +++ b/src/pages/ReimbursementAccount/BeneficialOwnersStep.js @@ -90,9 +90,11 @@ class BeneficialOwnersStep extends React.Component { const errors = {}; _.each(this.requiredFields, (inputKey) => { - if (!isRequiredFulfilled(this.state[inputKey])) { - errors[inputKey] = true; + if (isRequiredFulfilled(this.state[inputKey])) { + return; } + + errors[inputKey] = true; }); setBankAccountFormValidationErrors({...errors, beneficialOwnersErrors}); return _.every(beneficialOwnersErrors, _.isEmpty) && _.isEmpty(errors); diff --git a/src/pages/ReimbursementAccount/CompanyStep.js b/src/pages/ReimbursementAccount/CompanyStep.js index 9b95d709fa43..01790024fece 100644 --- a/src/pages/ReimbursementAccount/CompanyStep.js +++ b/src/pages/ReimbursementAccount/CompanyStep.js @@ -172,9 +172,11 @@ class CompanyStep extends React.Component { } _.each(this.requiredFields, (inputKey) => { - if (!isRequiredFulfilled(this.state[inputKey])) { - errors[inputKey] = true; + if (isRequiredFulfilled(this.state[inputKey])) { + return; } + + errors[inputKey] = true; }); setBankAccountFormValidationErrors(errors); return _.size(errors) === 0; diff --git a/src/pages/ReimbursementAccount/RequestorStep.js b/src/pages/ReimbursementAccount/RequestorStep.js index 64d7fa0f7bcc..32fa9c74c9f0 100644 --- a/src/pages/ReimbursementAccount/RequestorStep.js +++ b/src/pages/ReimbursementAccount/RequestorStep.js @@ -128,9 +128,11 @@ class RequestorStep extends React.Component { }); _.each(this.requiredFields, (inputKey) => { - if (!isRequiredFulfilled(this.state[inputKey])) { - errors[inputKey] = true; + if (isRequiredFulfilled(this.state[inputKey])) { + return; } + + errors[inputKey] = true; }); if (_.size(errors)) { setBankAccountFormValidationErrors(errors); diff --git a/src/pages/ReimbursementAccount/ValidationStep.js b/src/pages/ReimbursementAccount/ValidationStep.js index c6e47ce58015..a5546b19a039 100644 --- a/src/pages/ReimbursementAccount/ValidationStep.js +++ b/src/pages/ReimbursementAccount/ValidationStep.js @@ -101,9 +101,11 @@ class ValidationStep extends React.Component { }; _.each(this.requiredFields, (inputKey) => { - if (!isRequiredFulfilled(values[inputKey])) { - errors[inputKey] = true; + if (isRequiredFulfilled(values[inputKey])) { + return; } + + errors[inputKey] = true; }); setBankAccountFormValidationErrors(errors); return _.size(errors) === 0; diff --git a/src/pages/home/report/ContextMenu/ReportActionContextMenu.js b/src/pages/home/report/ContextMenu/ReportActionContextMenu.js index 58b900b9cb50..4cd97dda1199 100644 --- a/src/pages/home/report/ContextMenu/ReportActionContextMenu.js +++ b/src/pages/home/report/ContextMenu/ReportActionContextMenu.js @@ -63,9 +63,11 @@ function hideContextMenu(shouldDelay, onHideCallback = () => {}) { // If instance is not same, cancel the hide action const instanceID = contextMenuRef.current.instanceID; setTimeout(() => { - if (contextMenuRef.current.instanceID === instanceID) { - contextMenuRef.current.hideContextMenu(onHideCallback); + if (contextMenuRef.current.instanceID !== instanceID) { + return; } + + contextMenuRef.current.hideContextMenu(onHideCallback); }, 800); } diff --git a/src/pages/home/report/EmojiPickerMenu/index.js b/src/pages/home/report/EmojiPickerMenu/index.js index 18b6ad2da673..1c3bd85eabd7 100755 --- a/src/pages/home/report/EmojiPickerMenu/index.js +++ b/src/pages/home/report/EmojiPickerMenu/index.js @@ -105,53 +105,59 @@ class EmojiPickerMenu extends Component { * Setup and attach keypress/mouse handlers for highlight navigation. */ setupEventHandlers() { - if (document) { - this.keyDownHandler = (keyBoardEvent) => { - if (keyBoardEvent.key.startsWith('Arrow')) { - // Move the highlight when arrow keys are pressed - this.highlightAdjacentEmoji(keyBoardEvent.key); - return; - } - - // Select the currently highlighted emoji if enter is pressed - if (keyBoardEvent.key === 'Enter' && this.state.highlightedIndex !== -1) { - this.props.onEmojiSelected(this.state.filteredEmojis[this.state.highlightedIndex].code); - return; - } - - // We allow typing in the search box if any key is pressed apart from Arrow keys. - if (this.searchInput && !this.searchInput.isFocused()) { - this.setState({selectTextOnFocus: false}); - this.searchInput.value = ''; - this.searchInput.focus(); - - // Re-enable selection on the searchInput - this.setState({selectTextOnFocus: true}); - } - }; - - // Keyboard events are not bubbling on TextInput in RN-Web, Bubbling was needed for this event to trigger - // event handler attached to document root. To fix this, trigger event handler in Capture phase. - document.addEventListener('keydown', this.keyDownHandler, true); - - // Re-enable pointer events and hovering over EmojiPickerItems when the mouse moves - this.mouseMoveHandler = () => { - if (this.state.arePointerEventsDisabled) { - this.setState({arePointerEventsDisabled: false}); - } - }; - document.addEventListener('mousemove', this.mouseMoveHandler); + if (!document) { + return; } + + this.keyDownHandler = (keyBoardEvent) => { + if (keyBoardEvent.key.startsWith('Arrow')) { + // Move the highlight when arrow keys are pressed + this.highlightAdjacentEmoji(keyBoardEvent.key); + return; + } + + // Select the currently highlighted emoji if enter is pressed + if (keyBoardEvent.key === 'Enter' && this.state.highlightedIndex !== -1) { + this.props.onEmojiSelected(this.state.filteredEmojis[this.state.highlightedIndex].code); + return; + } + + // We allow typing in the search box if any key is pressed apart from Arrow keys. + if (this.searchInput && !this.searchInput.isFocused()) { + this.setState({selectTextOnFocus: false}); + this.searchInput.value = ''; + this.searchInput.focus(); + + // Re-enable selection on the searchInput + this.setState({selectTextOnFocus: true}); + } + }; + + // Keyboard events are not bubbling on TextInput in RN-Web, Bubbling was needed for this event to trigger + // event handler attached to document root. To fix this, trigger event handler in Capture phase. + document.addEventListener('keydown', this.keyDownHandler, true); + + // Re-enable pointer events and hovering over EmojiPickerItems when the mouse moves + this.mouseMoveHandler = () => { + if (!this.state.arePointerEventsDisabled) { + return; + } + + this.setState({arePointerEventsDisabled: false}); + }; + document.addEventListener('mousemove', this.mouseMoveHandler); } /** * Cleanup all mouse/keydown event listeners that we've set up */ cleanupEventHandlers() { - if (document) { - document.removeEventListener('keydown', this.keyDownHandler, true); - document.removeEventListener('mousemove', this.mouseMoveHandler); + if (!document) { + return; } + + document.removeEventListener('keydown', this.keyDownHandler, true); + document.removeEventListener('mousemove', this.mouseMoveHandler); } /** diff --git a/src/pages/home/report/ReportActionCompose.js b/src/pages/home/report/ReportActionCompose.js index a0ab399418c3..d421e8650cce 100755 --- a/src/pages/home/report/ReportActionCompose.js +++ b/src/pages/home/report/ReportActionCompose.js @@ -179,9 +179,11 @@ class ReportActionCompose extends React.Component { componentDidMount() { ReportActionComposeFocusManager.onComposerFocus(() => { - if (this.shouldFocusInputOnScreenFocus && this.props.isFocused) { - this.focus(false); + if (!this.shouldFocusInputOnScreenFocus || !this.props.isFocused) { + return; } + + this.focus(false); }); Dimensions.addEventListener('change', this.measureEmojiPopoverAnchorPosition); } @@ -218,9 +220,11 @@ class ReportActionCompose extends React.Component { * @param {Number|String} skinTone */ setPreferredSkinTone(skinTone) { - if (skinTone !== this.props.preferredSkinTone) { - User.setPreferredSkinTone(skinTone); + if (skinTone === this.props.preferredSkinTone) { + return; } + + User.setPreferredSkinTone(skinTone); } /** @@ -290,16 +294,18 @@ class ReportActionCompose extends React.Component { // There could be other animations running while we trigger manual focus. // This prevents focus from making those animations janky. InteractionManager.runAfterInteractions(() => { - if (this.textInput) { - if (!shouldelay) { - this.textInput.focus(); - } else { - // Keyboard is not opened after Emoji Picker is closed - // SetTimeout is used as a workaround - // https://github.com/react-native-modal/react-native-modal/issues/114 - // We carefully choose a delay. 50ms is found enough for keyboard to open. - setTimeout(() => this.textInput.focus(), 50); - } + if (!this.textInput) { + return; + } + + if (!shouldelay) { + this.textInput.focus(); + } else { + // Keyboard is not opened after Emoji Picker is closed + // SetTimeout is used as a workaround + // https://github.com/react-native-modal/react-native-modal/issues/114 + // We carefully choose a delay. 50ms is found enough for keyboard to open. + setTimeout(() => this.textInput.focus(), 50); } }); } @@ -356,26 +362,28 @@ class ReportActionCompose extends React.Component { * @param {Object} e */ triggerHotkeyActions(e) { - if (e) { - // Submit the form when Enter is pressed - if (e.key === 'Enter' && !e.shiftKey) { - e.preventDefault(); - this.submitForm(); - } + if (!e) { + return; + } - // Trigger the edit box for last sent message if ArrowUp is pressed - if (e.key === 'ArrowUp' && this.state.isCommentEmpty) { - e.preventDefault(); + // Submit the form when Enter is pressed + if (e.key === 'Enter' && !e.shiftKey) { + e.preventDefault(); + this.submitForm(); + } - const reportActionKey = _.find( - _.keys(this.props.reportActions).reverse(), - key => canEditReportAction(this.props.reportActions[key]), - ); + // Trigger the edit box for last sent message if ArrowUp is pressed + if (e.key === 'ArrowUp' && this.state.isCommentEmpty) { + e.preventDefault(); + + const reportActionKey = _.find( + _.keys(this.props.reportActions).reverse(), + key => canEditReportAction(this.props.reportActions[key]), + ); - if (reportActionKey !== -1 && this.props.reportActions[reportActionKey]) { - const {reportActionID, message} = this.props.reportActions[reportActionKey]; - saveReportActionDraft(this.props.reportID, reportActionID, _.last(message).html); - } + if (reportActionKey !== -1 && this.props.reportActions[reportActionKey]) { + const {reportActionID, message} = this.props.reportActions[reportActionKey]; + saveReportActionDraft(this.props.reportID, reportActionID, _.last(message).html); } } } @@ -393,11 +401,13 @@ class ReportActionCompose extends React.Component { * This gets called onLayout to find the cooridnates of the Anchor for the Emoji Picker. */ measureEmojiPopoverAnchorPosition() { - if (this.emojiPopoverAnchor) { - this.emojiPopoverAnchor.measureInWindow((x, y, width) => this.setState({ - emojiPopoverAnchorPosition: {horizontal: x + width, vertical: y}, - })); + if (!this.emojiPopoverAnchor) { + return; } + + this.emojiPopoverAnchor.measureInWindow((x, y, width) => this.setState({ + emojiPopoverAnchorPosition: {horizontal: x + width, vertical: y}, + })); } @@ -433,9 +443,11 @@ class ReportActionCompose extends React.Component { * Focus the search input in the emoji picker. */ focusEmojiSearchInput() { - if (this.emojiSearchInput) { - this.emojiSearchInput.focus(); + if (!this.emojiSearchInput) { + return; } + + this.emojiSearchInput.focus(); } /** @@ -611,14 +623,18 @@ class ReportActionCompose extends React.Component { onChangeText={this.updateComment} onKeyPress={this.triggerHotkeyActions} onDragEnter={(e, isOriginComposer) => { - if (isOriginComposer) { - this.setState({isDraggingOver: true}); + if (!isOriginComposer) { + return; } + + this.setState({isDraggingOver: true}); }} onDragOver={(e, isOriginComposer) => { - if (isOriginComposer) { - this.setState({isDraggingOver: true}); + if (!isOriginComposer) { + return; } + + this.setState({isDraggingOver: true}); }} onDragLeave={() => this.setState({isDraggingOver: false})} onDrop={(e) => { diff --git a/src/pages/home/report/ReportActionItem.js b/src/pages/home/report/ReportActionItem.js index 3a3c100ff692..2d1050f87cee 100644 --- a/src/pages/home/report/ReportActionItem.js +++ b/src/pages/home/report/ReportActionItem.js @@ -81,10 +81,12 @@ class ReportActionItem extends Component { } componentDidUpdate(prevProps) { - if (!prevProps.draftMessage && this.props.draftMessage) { - // Only focus the input when user edits a message, skip it for existing drafts being edited of the report. - this.textInput.focus(); + if (prevProps.draftMessage || !this.props.draftMessage) { + return; } + + // Only focus the input when user edits a message, skip it for existing drafts being edited of the report. + this.textInput.focus(); } /** diff --git a/src/pages/home/report/ReportActionsView.js b/src/pages/home/report/ReportActionsView.js index a32586f9ec64..84ee7d94afbf 100755 --- a/src/pages/home/report/ReportActionsView.js +++ b/src/pages/home/report/ReportActionsView.js @@ -123,9 +123,11 @@ class ReportActionsView extends React.Component { } Report.subscribeToReportTypingEvents(this.props.reportID); this.keyboardEvent = Keyboard.addListener('keyboardDidShow', () => { - if (ReportActionComposeFocusManager.isFocused()) { - this.scrollToListBottom(); + if (!ReportActionComposeFocusManager.isFocused()) { + return; } + + this.scrollToListBottom(); }); // Only mark as read if the report is open @@ -232,10 +234,11 @@ class ReportActionsView extends React.Component { * Records the max action on app visibility change event. */ onVisibilityChange() { - // only mark as read if visible AND report is open. - if (Visibility.isVisible() && !this.props.isDrawerOpen) { - Report.updateLastReadActionID(this.props.reportID); + if (!Visibility.isVisible() || this.props.isDrawerOpen) { + return; } + + Report.updateLastReadActionID(this.props.reportID); } /** diff --git a/src/pages/home/report/ReportTypingIndicator.js b/src/pages/home/report/ReportTypingIndicator.js index 10c811797f81..825a005089ad 100755 --- a/src/pages/home/report/ReportTypingIndicator.js +++ b/src/pages/home/report/ReportTypingIndicator.js @@ -33,13 +33,14 @@ class ReportTypingIndicator extends React.Component { componentDidUpdate(prevProps) { // Make sure we only update the state if there's been a change in who's typing. - if (!_.isEqual(prevProps.userTypingStatuses, this.props.userTypingStatuses)) { - const usersTyping = _.filter(_.keys(this.props.userTypingStatuses), login => this.props.userTypingStatuses[login]); - - // Suppressing because this is within a conditional, and hence we won't run into an infinite loop - // eslint-disable-next-line react/no-did-update-set-state - this.setState({usersTyping}); + if (_.isEqual(prevProps.userTypingStatuses, this.props.userTypingStatuses)) { + return; } + + const usersTyping = _.filter(_.keys(this.props.userTypingStatuses), login => this.props.userTypingStatuses[login]); + + // eslint-disable-next-line react/no-did-update-set-state + this.setState({usersTyping}); } render() { diff --git a/src/pages/home/sidebar/SidebarScreen.js b/src/pages/home/sidebar/SidebarScreen.js index 6f5ccbd80f48..d89624980029 100755 --- a/src/pages/home/sidebar/SidebarScreen.js +++ b/src/pages/home/sidebar/SidebarScreen.js @@ -67,23 +67,25 @@ class SidebarScreen extends Component { // NOTE: This setTimeout is required due to a bug in react-navigation where modals do not display properly in a drawerContent // This is a short-term workaround, see this issue for updates on a long-term solution: https://github.com/Expensify/App/issues/5296 setTimeout(() => { - if (this.props.isFirstTimeNewExpensifyUser) { - // If we are rendering the SidebarScreen at the same time as a workspace route that means we've already created a workspace via workspace/new and should not open the global - // create menu right now. - const routes = lodashGet(this.props.navigation.getState(), 'routes', []); - const topRouteName = lodashGet(_.last(routes), 'name', ''); - const isDisplayingWorkspaceRoute = topRouteName.toLowerCase().includes('workspace'); - - // It's also possible that we already have a workspace policy. In either case we will not toggle the menu but do still want to set the NVP in this case since the user does - // not need to create a workspace. - if (!Policy.isAdminOfFreePolicy(this.props.allPolicies) && !isDisplayingWorkspaceRoute) { - this.toggleCreateMenu(); - } - - // Set the NVP back to false so we don't automatically open the menu again - // Note: this may need to be moved if this NVP is used for anything else later - NameValuePair.set(CONST.NVP.IS_FIRST_TIME_NEW_EXPENSIFY_USER, false, ONYXKEYS.NVP_IS_FIRST_TIME_NEW_EXPENSIFY_USER); + if (!this.props.isFirstTimeNewExpensifyUser) { + return; } + + // If we are rendering the SidebarScreen at the same time as a workspace route that means we've already created a workspace via workspace/new and should not open the global + // create menu right now. + const routes = lodashGet(this.props.navigation.getState(), 'routes', []); + const topRouteName = lodashGet(_.last(routes), 'name', ''); + const isDisplayingWorkspaceRoute = topRouteName.toLowerCase().includes('workspace'); + + // It's also possible that we already have a workspace policy. In either case we will not toggle the menu but do still want to set the NVP in this case since the user does + // not need to create a workspace. + if (!Policy.isAdminOfFreePolicy(this.props.allPolicies) && !isDisplayingWorkspaceRoute) { + this.toggleCreateMenu(); + } + + // Set the NVP back to false so we don't automatically open the menu again + // Note: this may need to be moved if this NVP is used for anything else later + NameValuePair.set(CONST.NVP.IS_FIRST_TIME_NEW_EXPENSIFY_USER, false, ONYXKEYS.NVP_IS_FIRST_TIME_NEW_EXPENSIFY_USER); }, 1500); } diff --git a/src/pages/iou/IOUCurrencySelection.js b/src/pages/iou/IOUCurrencySelection.js index bea865db7310..a0a3c2ae0e91 100644 --- a/src/pages/iou/IOUCurrencySelection.js +++ b/src/pages/iou/IOUCurrencySelection.js @@ -159,9 +159,11 @@ class IOUCurrencySelection extends Component { render() { return ( { - if (this.textInput) { - this.textInput.focus(); + if (!this.textInput) { + return; } + + this.textInput.focus(); }} > diff --git a/src/pages/iou/IOUTransactions.js b/src/pages/iou/IOUTransactions.js index 2bb0ca7e9dc2..00e87170d7a4 100644 --- a/src/pages/iou/IOUTransactions.js +++ b/src/pages/iou/IOUTransactions.js @@ -75,25 +75,26 @@ class IOUTransactions extends Component { return ( {_.map(this.props.reportActions, (reportAction) => { - if (reportAction.originalMessage - && reportAction.originalMessage.IOUReportID === this.props.iouReportID) { - const rejectableTransactions = this.getRejectableTransactions(); - const canBeRejected = _.contains(rejectableTransactions, - reportAction.originalMessage.IOUTransactionID); - const isCurrentUserTransactionCreator = this.props.userEmail === reportAction.actorEmail; - return ( - - ); + if (!reportAction.originalMessage || reportAction.originalMessage.IOUReportID !== this.props.iouReportID) { + return; } + + const rejectableTransactions = this.getRejectableTransactions(); + const canBeRejected = _.contains(rejectableTransactions, + reportAction.originalMessage.IOUTransactionID); + const isCurrentUserTransactionCreator = this.props.userEmail === reportAction.actorEmail; + return ( + + ); })} ); diff --git a/src/pages/iou/steps/IOUAmountPage.js b/src/pages/iou/steps/IOUAmountPage.js index 386ac2930b79..df18c505b926 100755 --- a/src/pages/iou/steps/IOUAmountPage.js +++ b/src/pages/iou/steps/IOUAmountPage.js @@ -88,9 +88,11 @@ class IOUAmountPage extends React.Component { } componentDidUpdate(prevProps) { - if (this.props.iou.selectedCurrencyCode !== prevProps.iou.selectedCurrencyCode) { - this.focusTextInput(); + if (this.props.iou.selectedCurrencyCode === prevProps.iou.selectedCurrencyCode) { + return; } + + this.focusTextInput(); } /** @@ -101,9 +103,11 @@ class IOUAmountPage extends React.Component { // Wait until interactions are complete before trying to focus InteractionManager.runAfterInteractions(() => { // Focus text input - if (this.textInput) { - this.textInput.focus(); + if (!this.textInput) { + return; } + + this.textInput.focus(); }); } @@ -158,9 +162,11 @@ class IOUAmountPage extends React.Component { * @param {String} amount */ updateAmount(amount) { - if (this.validateAmount(amount)) { - this.setState({amount: this.stripCommaFromAmount(amount)}); + if (!this.validateAmount(amount)) { + return; } + + this.setState({amount: this.stripCommaFromAmount(amount)}); } render() { diff --git a/src/pages/settings/AddSecondaryLoginPage.js b/src/pages/settings/AddSecondaryLoginPage.js index 904b872a7a5b..74360c088f8e 100755 --- a/src/pages/settings/AddSecondaryLoginPage.js +++ b/src/pages/settings/AddSecondaryLoginPage.js @@ -91,9 +91,11 @@ class AddSecondaryLoginPage extends Component { render() { return ( { - if (this.phoneNumberInputRef) { - this.phoneNumberInputRef.focus(); + if (!this.phoneNumberInputRef) { + return; } + + this.phoneNumberInputRef.focus(); }} > diff --git a/src/pages/settings/PasswordPage.js b/src/pages/settings/PasswordPage.js index 940ba49218fb..f5511bc0cd09 100755 --- a/src/pages/settings/PasswordPage.js +++ b/src/pages/settings/PasswordPage.js @@ -104,9 +104,11 @@ class PasswordPage extends Component { render() { return ( { - if (this.currentPasswordInputRef) { - this.currentPasswordInputRef.focus(); + if (!this.currentPasswordInputRef) { + return; } + + this.currentPasswordInputRef.focus(); }} > diff --git a/src/pages/settings/Payments/AddPayPalMePage.js b/src/pages/settings/Payments/AddPayPalMePage.js index fa59cb8fecba..2b81dc96ab81 100644 --- a/src/pages/settings/Payments/AddPayPalMePage.js +++ b/src/pages/settings/Payments/AddPayPalMePage.js @@ -48,11 +48,12 @@ class AddPayPalMePage extends React.Component { } componentDidUpdate(prevProps) { - if (prevProps.payPalMeUsername !== this.props.payPalMeUsername) { - // Suppressing because this is within a conditional, and hence we won't run into an infinite loop - // eslint-disable-next-line react/no-did-update-set-state - this.setState({payPalMeUsername: this.props.payPalMeUsername}); + if (prevProps.payPalMeUsername === this.props.payPalMeUsername) { + return; } + + // eslint-disable-next-line react/no-did-update-set-state + this.setState({payPalMeUsername: this.props.payPalMeUsername}); } /** diff --git a/src/pages/settings/Payments/PaymentMethodList.js b/src/pages/settings/Payments/PaymentMethodList.js index adcada2864ab..738f1a893e35 100644 --- a/src/pages/settings/Payments/PaymentMethodList.js +++ b/src/pages/settings/Payments/PaymentMethodList.js @@ -75,25 +75,27 @@ class PaymentMethodList extends Component { _.each(this.props.bankAccountList, (bankAccount) => { // Add all bank accounts besides the wallet - if (bankAccount.type !== CONST.BANK_ACCOUNT_TYPES.WALLET) { - const formattedBankAccountNumber = bankAccount.accountNumber - ? `${this.props.translate('paymentMethodList.accountLastFour')} ${ - bankAccount.accountNumber.slice(-4) - }` - : null; - const {icon, iconSize} = getBankIcon(lodashGet(bankAccount, 'additionalData.bankName', '')); - combinedPaymentMethods.push({ - type: MENU_ITEM, - title: bankAccount.addressName, - - // eslint-disable-next-line - description: formattedBankAccountNumber, - icon, - iconSize, - onPress: e => this.props.onPress(e, bankAccount.bankAccountID), - key: `bankAccount-${bankAccount.bankAccountID}`, - }); + if (bankAccount.type === CONST.BANK_ACCOUNT_TYPES.WALLET) { + return; } + + const formattedBankAccountNumber = bankAccount.accountNumber + ? `${this.props.translate('paymentMethodList.accountLastFour')} ${ + bankAccount.accountNumber.slice(-4) + }` + : null; + const {icon, iconSize} = getBankIcon(lodashGet(bankAccount, 'additionalData.bankName', '')); + combinedPaymentMethods.push({ + type: MENU_ITEM, + title: bankAccount.addressName, + + // eslint-disable-next-line + description: formattedBankAccountNumber, + icon, + iconSize, + onPress: e => this.props.onPress(e, bankAccount.bankAccountID), + key: `bankAccount-${bankAccount.bankAccountID}`, + }); }); _.each(this.props.cardList, (card) => { diff --git a/src/pages/settings/Profile/LoginField.js b/src/pages/settings/Profile/LoginField.js index 1c4c1d93e778..314507a2c0cb 100755 --- a/src/pages/settings/Profile/LoginField.js +++ b/src/pages/settings/Profile/LoginField.js @@ -53,10 +53,12 @@ class LoginField extends Component { // Revert checkmark back to "Resend" after 5 seconds if (!this.timeout) { this.timeout = setTimeout(() => { - if (this.timeout) { - this.setState({showCheckmarkIcon: false}); - this.timeout = null; + if (!this.timeout) { + return; } + + this.setState({showCheckmarkIcon: false}); + this.timeout = null; }, 5000); } } diff --git a/src/pages/settings/Profile/ProfilePage.js b/src/pages/settings/Profile/ProfilePage.js index 173b483f42ec..e54ed48a68d7 100755 --- a/src/pages/settings/Profile/ProfilePage.js +++ b/src/pages/settings/Profile/ProfilePage.js @@ -99,12 +99,14 @@ class ProfilePage extends Component { componentDidUpdate(prevProps) { // Recalculate logins if loginList has changed - if (this.props.user.loginList !== prevProps.user.loginList) { - // eslint-disable-next-line react/no-did-update-set-state - this.setState({ - logins: this.getLogins(this.props.user.loginList), - }); + if (this.props.user.loginList === prevProps.user.loginList) { + return; } + + // eslint-disable-next-line react/no-did-update-set-state + this.setState({ + logins: this.getLogins(this.props.user.loginList), + }); } /** @@ -187,11 +189,10 @@ class ProfilePage extends Component { } render() { - const pronounsList = Object.entries(this.props.translate('pronouns')) - .map(([key, value]) => ({ - label: value, - value: `${CONST.PRONOUNS.PREFIX}${key}`, - })); + const pronounsList = _.map(this.props.translate('pronouns'), (value, key) => ({ + label: value, + value: `${CONST.PRONOUNS.PREFIX}${key}`, + })); // Determines if the pronouns/selected pronouns have changed const arePronounsUnchanged = this.props.myPersonalDetails.pronouns === this.state.pronouns; diff --git a/src/pages/signin/ResendValidationForm.js b/src/pages/signin/ResendValidationForm.js index ecd9644e0a7f..27c63c217a63 100755 --- a/src/pages/signin/ResendValidationForm.js +++ b/src/pages/signin/ResendValidationForm.js @@ -58,9 +58,11 @@ class ResendValidationForm extends React.Component { } componentWillUnmount() { - if (this.successMessageTimer) { - clearTimeout(this.successMessageTimer); + if (!this.successMessageTimer) { + return; } + + clearTimeout(this.successMessageTimer); } /** diff --git a/src/setup/platformSetup/index.website.js b/src/setup/platformSetup/index.website.js index 754718595ceb..2913d8ea52ea 100644 --- a/src/setup/platformSetup/index.website.js +++ b/src/setup/platformSetup/index.website.js @@ -13,17 +13,19 @@ import Visibility from '../../libs/Visibility'; function webUpdate() { HttpUtils.download('version.json') .then(({version}) => { - if (version !== currentVersion) { - if (!Visibility.isVisible()) { - // Page is hidden, refresh immediately - window.location.reload(true); - return; - } - - // Prompt user to refresh the page - if (window.confirm('Refresh the page to get the latest updates!')) { - window.location.reload(true); - } + if (version === currentVersion) { + return; + } + + if (!Visibility.isVisible()) { + // Page is hidden, refresh immediately + window.location.reload(true); + return; + } + + // Prompt user to refresh the page + if (window.confirm('Refresh the page to get the latest updates!')) { + window.location.reload(true); } }); } @@ -39,9 +41,11 @@ const webUpdater = () => ({ // That way, it will auto-update silently when they minimize the page, // and we don't bug the user any more than necessary :) window.addEventListener('visibilitychange', () => { - if (!Visibility.isVisible()) { - webUpdate(); + if (Visibility.isVisible()) { + return; } + + webUpdate(); }); }, update: () => webUpdate(), diff --git a/src/styles/getReportActionItemStyles.js b/src/styles/getReportActionItemStyles.js index 5db9ed341ae8..122581d384ff 100644 --- a/src/styles/getReportActionItemStyles.js +++ b/src/styles/getReportActionItemStyles.js @@ -8,7 +8,7 @@ import positioning from './utilities/positioning'; * @param {Boolean} [isHovered] * @returns {Object} */ -export function getReportActionItemStyle(isHovered = false) { +function getReportActionItemStyle(isHovered = false) { return { display: 'flex', justifyContent: 'space-between', @@ -27,7 +27,7 @@ export function getReportActionItemStyle(isHovered = false) { * @param {Boolean} isReportActionItemGrouped * @returns {Object} */ -export function getMiniReportActionContextMenuWrapperStyle(isReportActionItemGrouped) { +function getMiniReportActionContextMenuWrapperStyle(isReportActionItemGrouped) { return { ...(isReportActionItemGrouped ? positioning.tn8 : positioning.tn4), ...positioning.r4, @@ -35,3 +35,8 @@ export function getMiniReportActionContextMenuWrapperStyle(isReportActionItemGro zIndex: 1, }; } + +export { + getMiniReportActionContextMenuWrapperStyle, + getReportActionItemStyle, +}; diff --git a/tests/unit/versionUpdaterTest.js b/tests/unit/versionUpdaterTest.js index 994fb0d79d94..b9347f9341d3 100644 --- a/tests/unit/versionUpdaterTest.js +++ b/tests/unit/versionUpdaterTest.js @@ -1,4 +1,4 @@ -const versionUpdater = require('../../.github/libs/versionUpdater.js'); +const versionUpdater = require('../../.github/libs/versionUpdater'); const VERSION = '2.3.9-80'; const VERSION_NUMBER = [2, 3, 9, 80];