diff --git a/.github/actions/checkDeployBlockers/checkDeployBlockers.js b/.github/actions/checkDeployBlockers/checkDeployBlockers.js index 000ea410b94d..c0c50d2cce4e 100644 --- a/.github/actions/checkDeployBlockers/checkDeployBlockers.js +++ b/.github/actions/checkDeployBlockers/checkDeployBlockers.js @@ -16,9 +16,8 @@ const run = function () { console.log('Checking for unverified PRs or unresolved deploy blockers', data); // Check the issue description to see if there are any unfinished/un-QAed items in the checklist. - const uncheckedBoxRegex = /-\s\[\s]/g; - const matches = uncheckedBoxRegex.exec(data.body); - if (matches !== null) { + const uncheckedBoxRegex = new RegExp(`-\\s\\[\\s]\\s(?:QA|${GithubUtils.ISSUE_OR_PULL_REQUEST_REGEX.source})`); + if (uncheckedBoxRegex.test(data.body)) { console.log('An unverified PR or unresolved deploy blocker was found.'); core.setOutput('HAS_DEPLOY_BLOCKERS', true); return; diff --git a/.github/actions/checkDeployBlockers/index.js b/.github/actions/checkDeployBlockers/index.js index 2162f09f07f5..79cbf3100e5b 100644 --- a/.github/actions/checkDeployBlockers/index.js +++ b/.github/actions/checkDeployBlockers/index.js @@ -26,9 +26,8 @@ const run = function () { console.log('Checking for unverified PRs or unresolved deploy blockers', data); // Check the issue description to see if there are any unfinished/un-QAed items in the checklist. - const uncheckedBoxRegex = /-\s\[\s]/g; - const matches = uncheckedBoxRegex.exec(data.body); - if (matches !== null) { + const uncheckedBoxRegex = new RegExp(`-\\s\\[\\s]\\s(?:QA|${GithubUtils.ISSUE_OR_PULL_REQUEST_REGEX.source})`); + if (uncheckedBoxRegex.test(data.body)) { console.log('An unverified PR or unresolved deploy blocker was found.'); core.setOutput('HAS_DEPLOY_BLOCKERS', true); return; @@ -512,6 +511,7 @@ module.exports.EXPENSIFY_CASH_REPO = EXPENSIFY_CASH_REPO; module.exports.STAGING_DEPLOY_CASH_LABEL = STAGING_DEPLOY_CASH_LABEL; module.exports.DEPLOY_BLOCKER_CASH_LABEL = DEPLOY_BLOCKER_CASH_LABEL; module.exports.APPLAUSE_BOT = APPLAUSE_BOT; +module.exports.ISSUE_OR_PULL_REQUEST_REGEX = ISSUE_OR_PULL_REQUEST_REGEX; /***/ }), diff --git a/.github/actions/createOrUpdateStagingDeploy/index.js b/.github/actions/createOrUpdateStagingDeploy/index.js index 613ccb0401cb..1fc4bf16c44f 100644 --- a/.github/actions/createOrUpdateStagingDeploy/index.js +++ b/.github/actions/createOrUpdateStagingDeploy/index.js @@ -701,6 +701,7 @@ module.exports.EXPENSIFY_CASH_REPO = EXPENSIFY_CASH_REPO; module.exports.STAGING_DEPLOY_CASH_LABEL = STAGING_DEPLOY_CASH_LABEL; module.exports.DEPLOY_BLOCKER_CASH_LABEL = DEPLOY_BLOCKER_CASH_LABEL; module.exports.APPLAUSE_BOT = APPLAUSE_BOT; +module.exports.ISSUE_OR_PULL_REQUEST_REGEX = ISSUE_OR_PULL_REQUEST_REGEX; /***/ }), diff --git a/.github/actions/getPullRequestDetails/index.js b/.github/actions/getPullRequestDetails/index.js index 6e9e8b9fe4cf..342373eebb12 100644 --- a/.github/actions/getPullRequestDetails/index.js +++ b/.github/actions/getPullRequestDetails/index.js @@ -565,6 +565,7 @@ module.exports.EXPENSIFY_CASH_REPO = EXPENSIFY_CASH_REPO; module.exports.STAGING_DEPLOY_CASH_LABEL = STAGING_DEPLOY_CASH_LABEL; module.exports.DEPLOY_BLOCKER_CASH_LABEL = DEPLOY_BLOCKER_CASH_LABEL; module.exports.APPLAUSE_BOT = APPLAUSE_BOT; +module.exports.ISSUE_OR_PULL_REQUEST_REGEX = ISSUE_OR_PULL_REQUEST_REGEX; /***/ }), diff --git a/.github/actions/getReleaseBody/index.js b/.github/actions/getReleaseBody/index.js index 794b52333290..e34ba2955069 100644 --- a/.github/actions/getReleaseBody/index.js +++ b/.github/actions/getReleaseBody/index.js @@ -483,6 +483,7 @@ module.exports.EXPENSIFY_CASH_REPO = EXPENSIFY_CASH_REPO; module.exports.STAGING_DEPLOY_CASH_LABEL = STAGING_DEPLOY_CASH_LABEL; module.exports.DEPLOY_BLOCKER_CASH_LABEL = DEPLOY_BLOCKER_CASH_LABEL; module.exports.APPLAUSE_BOT = APPLAUSE_BOT; +module.exports.ISSUE_OR_PULL_REQUEST_REGEX = ISSUE_OR_PULL_REQUEST_REGEX; /***/ }), diff --git a/.github/actions/isPullRequestMergeable/index.js b/.github/actions/isPullRequestMergeable/index.js index d6e271850742..e8f8e84cacea 100644 --- a/.github/actions/isPullRequestMergeable/index.js +++ b/.github/actions/isPullRequestMergeable/index.js @@ -501,6 +501,7 @@ module.exports.EXPENSIFY_CASH_REPO = EXPENSIFY_CASH_REPO; module.exports.STAGING_DEPLOY_CASH_LABEL = STAGING_DEPLOY_CASH_LABEL; module.exports.DEPLOY_BLOCKER_CASH_LABEL = DEPLOY_BLOCKER_CASH_LABEL; module.exports.APPLAUSE_BOT = APPLAUSE_BOT; +module.exports.ISSUE_OR_PULL_REQUEST_REGEX = ISSUE_OR_PULL_REQUEST_REGEX; /***/ }), diff --git a/.github/actions/isStagingDeployLocked/index.js b/.github/actions/isStagingDeployLocked/index.js index 5a0e38ed3de3..7ea200ca6ff0 100644 --- a/.github/actions/isStagingDeployLocked/index.js +++ b/.github/actions/isStagingDeployLocked/index.js @@ -464,6 +464,7 @@ module.exports.EXPENSIFY_CASH_REPO = EXPENSIFY_CASH_REPO; module.exports.STAGING_DEPLOY_CASH_LABEL = STAGING_DEPLOY_CASH_LABEL; module.exports.DEPLOY_BLOCKER_CASH_LABEL = DEPLOY_BLOCKER_CASH_LABEL; module.exports.APPLAUSE_BOT = APPLAUSE_BOT; +module.exports.ISSUE_OR_PULL_REQUEST_REGEX = ISSUE_OR_PULL_REQUEST_REGEX; /***/ }), diff --git a/.github/actions/markPullRequestsAsDeployed/index.js b/.github/actions/markPullRequestsAsDeployed/index.js index 9fc180ac5ec9..cfb143b877a3 100644 --- a/.github/actions/markPullRequestsAsDeployed/index.js +++ b/.github/actions/markPullRequestsAsDeployed/index.js @@ -616,6 +616,7 @@ module.exports.EXPENSIFY_CASH_REPO = EXPENSIFY_CASH_REPO; module.exports.STAGING_DEPLOY_CASH_LABEL = STAGING_DEPLOY_CASH_LABEL; module.exports.DEPLOY_BLOCKER_CASH_LABEL = DEPLOY_BLOCKER_CASH_LABEL; module.exports.APPLAUSE_BOT = APPLAUSE_BOT; +module.exports.ISSUE_OR_PULL_REQUEST_REGEX = ISSUE_OR_PULL_REQUEST_REGEX; /***/ }), diff --git a/.github/actions/reopenIssueWithComment/index.js b/.github/actions/reopenIssueWithComment/index.js index a7ce41077ef5..4b34fae9bfce 100644 --- a/.github/actions/reopenIssueWithComment/index.js +++ b/.github/actions/reopenIssueWithComment/index.js @@ -475,6 +475,7 @@ module.exports.EXPENSIFY_CASH_REPO = EXPENSIFY_CASH_REPO; module.exports.STAGING_DEPLOY_CASH_LABEL = STAGING_DEPLOY_CASH_LABEL; module.exports.DEPLOY_BLOCKER_CASH_LABEL = DEPLOY_BLOCKER_CASH_LABEL; module.exports.APPLAUSE_BOT = APPLAUSE_BOT; +module.exports.ISSUE_OR_PULL_REQUEST_REGEX = ISSUE_OR_PULL_REQUEST_REGEX; /***/ }), diff --git a/.github/actions/triggerWorkflowAndWait/index.js b/.github/actions/triggerWorkflowAndWait/index.js index 9ba263668e8e..7f1a048fe333 100644 --- a/.github/actions/triggerWorkflowAndWait/index.js +++ b/.github/actions/triggerWorkflowAndWait/index.js @@ -634,6 +634,7 @@ module.exports.EXPENSIFY_CASH_REPO = EXPENSIFY_CASH_REPO; module.exports.STAGING_DEPLOY_CASH_LABEL = STAGING_DEPLOY_CASH_LABEL; module.exports.DEPLOY_BLOCKER_CASH_LABEL = DEPLOY_BLOCKER_CASH_LABEL; module.exports.APPLAUSE_BOT = APPLAUSE_BOT; +module.exports.ISSUE_OR_PULL_REQUEST_REGEX = ISSUE_OR_PULL_REQUEST_REGEX; /***/ }), diff --git a/.github/libs/GithubUtils.js b/.github/libs/GithubUtils.js index d750c7340ccb..a17a2b875543 100644 --- a/.github/libs/GithubUtils.js +++ b/.github/libs/GithubUtils.js @@ -424,3 +424,4 @@ module.exports.EXPENSIFY_CASH_REPO = EXPENSIFY_CASH_REPO; module.exports.STAGING_DEPLOY_CASH_LABEL = STAGING_DEPLOY_CASH_LABEL; module.exports.DEPLOY_BLOCKER_CASH_LABEL = DEPLOY_BLOCKER_CASH_LABEL; module.exports.APPLAUSE_BOT = APPLAUSE_BOT; +module.exports.ISSUE_OR_PULL_REQUEST_REGEX = ISSUE_OR_PULL_REQUEST_REGEX; diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 8b9de823d1b5..7753bc0d30cb 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -12,6 +12,10 @@ You can create as many accounts as needed in order to test your changes directly ##### Generating Multiple Test Accounts You can generate multiple test accounts by using a `+` postfix, for example if your email is test@test.com, you can create multiple New Expensify accounts connected to the same email address by using test+123@test.com, test+456@test.com, etc. +#### Working on beta features + +Some features are locked behind beta flags while development is ongoing. As a contributor you can work on these beta features locally by overriding the [`Permissions.canUseAllBetas` function](https://github.com/Expensify/App/blob/5e268df7f2989ed04bc64c0c86ed77faf134554d/src/libs/Permissions.js#L10-L12) to return `true`. + ## Code of Conduct This project and everyone participating in it is governed by the Expensify [Code of Conduct](https://github.com/Expensify/App/blob/main/CODE_OF_CONDUCT.md). By participating, you are expected to uphold this code. Please report unacceptable behavior to [contributors@expensify.com](mailto:contributors@expensify.com). diff --git a/android/app/build.gradle b/android/app/build.gradle index e374f6e0e982..eaadee9751c7 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -152,8 +152,8 @@ android { minSdkVersion rootProject.ext.minSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion multiDexEnabled rootProject.ext.multiDexEnabled - versionCode 1001013200 - versionName "1.1.32-0" + versionCode 1001013201 + versionName "1.1.32-1" } splits { abi { diff --git a/ios/NewExpensify/Info.plist b/ios/NewExpensify/Info.plist index 579c34777417..c59bd66518e2 100644 --- a/ios/NewExpensify/Info.plist +++ b/ios/NewExpensify/Info.plist @@ -31,7 +31,7 @@ CFBundleVersion - 1.1.32.0 + 1.1.32.1 ITSAppUsesNonExemptEncryption LSApplicationQueriesSchemes diff --git a/ios/NewExpensifyTests/Info.plist b/ios/NewExpensifyTests/Info.plist index 0716a4531166..492062abb533 100644 --- a/ios/NewExpensifyTests/Info.plist +++ b/ios/NewExpensifyTests/Info.plist @@ -19,6 +19,6 @@ CFBundleSignature ???? CFBundleVersion - 1.1.32.0 + 1.1.32.1 diff --git a/package-lock.json b/package-lock.json index 51b672317a7c..4de489ed0f3d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "new.expensify", - "version": "1.1.32-0", + "version": "1.1.32-1", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -37116,17 +37116,17 @@ "integrity": "sha1-RvGKMgQyCYol6p+on1FD3SVNMy0=" }, "react-native-modal": { - "version": "11.10.0", - "resolved": "https://registry.npmjs.org/react-native-modal/-/react-native-modal-11.10.0.tgz", - "integrity": "sha512-syRYDJYSh16bR37R5EKU9T/wC+5bEOfF17IUqf5URdhbEDd+hxyMInC++l45E8oI+MtdOaEp9yAws5xDqk8dnA==", + "version": "13.0.0", + "resolved": "https://registry.npmjs.org/react-native-modal/-/react-native-modal-13.0.0.tgz", + "integrity": "sha512-k6r9T31mc7HIDFj1V53ceAAN1dwc8052c4JLtDVEmEQ19Bbq9yiLXoDsQuNb+hB8A+2tVOXmo5Gq4IQfb11upw==", "requires": { "prop-types": "^15.6.2", "react-native-animatable": "1.3.3" } }, "react-native-onyx": { - "version": "git+https://github.com/Expensify/react-native-onyx.git#40c5b14dcc77e1193d027ecc9dd5f2563516e148", - "from": "git+https://github.com/Expensify/react-native-onyx.git#40c5b14dcc77e1193d027ecc9dd5f2563516e148", + "version": "git+https://github.com/Expensify/react-native-onyx.git#f3814ff18405e7021c1bb51011396ce37474df86", + "from": "git+https://github.com/Expensify/react-native-onyx.git#f3814ff18405e7021c1bb51011396ce37474df86", "requires": { "ascii-table": "0.0.9", "expensify-common": "git+https://github.com/Expensify/expensify-common.git#2e5cff552cf132da90a3fb9756e6b4fb6ae7b40c", diff --git a/package.json b/package.json index 1184f35c2fb0..dc3ecd2cb348 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "new.expensify", - "version": "1.1.32-0", + "version": "1.1.32-1", "author": "Expensify, Inc.", "homepage": "https://new.expensify.com", "description": "New Expensify is the next generation of Expensify: a reimagination of payments based atop a foundation of chat.", @@ -93,8 +93,8 @@ "react-native-image-picker": "^4.1.2", "react-native-image-size": "^1.1.3", "react-native-keyboard-spacer": "^0.4.1", - "react-native-modal": "^11.10.0", - "react-native-onyx": "git+https://github.com/Expensify/react-native-onyx.git#40c5b14dcc77e1193d027ecc9dd5f2563516e148", + "react-native-modal": "^13.0.0", + "react-native-onyx": "git+https://github.com/Expensify/react-native-onyx.git#f3814ff18405e7021c1bb51011396ce37474df86", "react-native-pdf": "^6.2.2", "react-native-performance": "^2.0.0", "react-native-permissions": "^3.0.1", diff --git a/src/CONST.js b/src/CONST.js index 42580b06b157..b449636dfd57 100755 --- a/src/CONST.js +++ b/src/CONST.js @@ -213,6 +213,7 @@ const CONST = { TYPE: { IOU: 'IOU', ADDCOMMENT: 'ADDCOMMENT', + RENAMED: 'RENAMED', }, }, ERROR: { diff --git a/src/ONYXKEYS.js b/src/ONYXKEYS.js index 2e77064d3f87..e701f99ab6a1 100755 --- a/src/ONYXKEYS.js +++ b/src/ONYXKEYS.js @@ -166,6 +166,9 @@ export default { // Are we loading the create policy room command IS_LOADING_CREATE_POLICY_ROOM: 'isLoadingCratePolicyRoom', + // Are we loading the rename policy room command + IS_LOADING_RENAME_POLICY_ROOM: 'isLoadingRenamePolicyRoom', + // Is Keyboard shortcuts modal open? IS_SHORTCUTS_MODAL_OPEN: 'isShortcutsModalOpen', @@ -177,4 +180,7 @@ export default { // The policyID of the last workspace whose settings were accessed by the user LAST_ACCESSED_WORKSPACE_POLICY_ID: 'lastAccessedWorkspacePolicyID', + + // Validating Email? + USER_SIGN_UP: 'userSignUp', }; diff --git a/src/components/AddPlaidBankAccount.js b/src/components/AddPlaidBankAccount.js index 8f72a23afad5..f3837f8ad61d 100644 --- a/src/components/AddPlaidBankAccount.js +++ b/src/components/AddPlaidBankAccount.js @@ -117,7 +117,7 @@ class AddPlaidBankAccount extends React.Component { this.getErrors = () => ReimbursementAccountUtils.getErrors(this.props); this.clearError = inputKey => ReimbursementAccountUtils.clearError(this.props, inputKey); this.getErrorText = inputKey => ReimbursementAccountUtils.getErrorText(this.props, { - password: 'passwordForm.error.incorrectLoginOrPassword', + password: 'passwordForm.error.incorrectPassword', }, inputKey); } diff --git a/src/components/ImageView/index.native.js b/src/components/ImageView/index.native.js index badf3687ccc3..461849319b56 100644 --- a/src/components/ImageView/index.native.js +++ b/src/components/ImageView/index.native.js @@ -69,6 +69,10 @@ class ImageView extends PureComponent { imageWidth = Math.round(this.props.windowWidth); imageHeight = Math.round(imageWidth * aspectRatio); } + + // Resize the image to max dimensions possible on the Native platforms to prevent crashes on Android. To keep the same behavior, apply to IOS as well. + const maxDimensionsScale = 11; + imageHeight = Math.min(imageHeight, (this.props.windowHeight * maxDimensionsScale)); this.setState({imageHeight, imageWidth}); }); } diff --git a/src/components/ReportActionItem/RenameAction.js b/src/components/ReportActionItem/RenameAction.js new file mode 100644 index 000000000000..4b83ba66a610 --- /dev/null +++ b/src/components/ReportActionItem/RenameAction.js @@ -0,0 +1,34 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import lodashGet from 'lodash/get'; +import Text from '../Text'; +import styles from '../../styles/styles'; +import reportActionPropTypes from '../../pages/home/report/reportActionPropTypes'; +import withLocalize, {withLocalizePropTypes} from '../withLocalize'; + +const propTypes = { + /** All the data of the action */ + action: PropTypes.shape(reportActionPropTypes).isRequired, + + ...withLocalizePropTypes, +}; + +const RenameAction = (props) => { + const displayName = lodashGet(props.action, ['message', 0, 'text']); + const oldName = lodashGet(props.action, 'originalMessage.oldName', ''); + const newName = lodashGet(props.action, 'originalMessage.newName', ''); + + return ( + + + {displayName} + + {props.translate('newRoomPage.renamedRoomAction', {oldName, newName})} + + ); +}; + +RenameAction.propTypes = propTypes; +RenameAction.displayName = 'RenameAction'; + +export default withLocalize(RenameAction); diff --git a/src/languages/en.js b/src/languages/en.js index 1520446a1498..f5b321abe683 100755 --- a/src/languages/en.js +++ b/src/languages/en.js @@ -381,8 +381,8 @@ export default { }, paymentMethodList: { addPaymentMethod: 'Add payment method', - addDebitCard: 'Add debit card', - addBankAccount: 'Add bank account', + addNewDebitCard: 'Add new debit card', + addNewBankAccount: 'Add new bank account', accountLastFour: 'Account ending in', cardLastFour: 'Card ending in', addFirstPaymentMethod: 'Add a payment method to send and receive payments directly in the app.', @@ -432,6 +432,7 @@ export default { twoFactorCode: 'Two factor code', requiredWhen2FAEnabled: 'Required when 2FA is enabled', error: { + incorrectPassword: 'Incorrect password. Please try again.', incorrectLoginOrPassword: 'Incorrect login or password. Please try again.', twoFactorAuthenticationEnabled: 'You have 2FA enabled on this account. Please sign in using your email or phone number.', invalidLoginOrPassword: 'Invalid login or password. Please try again or reset your password.', @@ -478,8 +479,9 @@ export default { setPassword: 'Set password', newPasswordPrompt: 'Your password must have at least 8 characters, 1 capital letter, 1 lowercase letter, and 1 number.', passwordFormTitle: 'Welcome back to the New Expensify! Please set your password.', - passwordNotSet: 'We were unable to set your new password correctly.', - accountNotValidated: 'We were unable to validate your account. The validation code may have expired.', + passwordNotSet: 'We were unable to set your new password. We have sent you a new password link to try again.', + setPasswordLinkInvalid: 'This set password link is invalid or has expired. A new one is waiting for you in your email inbox!', + verifyingAccount: 'Verifying account', }, stepCounter: ({step, total}) => `Step ${step} of ${total}`, bankAccount: { @@ -886,11 +888,14 @@ export default { restrictedDescription: 'People in your workspace are able to find this room using Search', privateDescription: 'Only people invited to this room are able to find it', createRoom: 'Create Room', + policyRoomRenamed: 'Policy room renamed!', roomAlreadyExistsError: 'A room with this name already exists', roomNameReservedError: 'This name is reserved and cannot be used', + renamedRoomAction: ({oldName, newName}) => ` renamed this room from ${oldName} to ${newName}`, social: 'social', selectAWorkspace: 'Select a workspace', growlMessageOnError: 'Unable to create policy room, please check your connection and try again.', + growlMessageOnRenameError: 'Unable to rename policy room, please check your connection and try again.', visibilityOptions: { restricted: 'Restricted', private: 'Private', diff --git a/src/languages/es.js b/src/languages/es.js index 627dd67ccda3..6276a13be41b 100644 --- a/src/languages/es.js +++ b/src/languages/es.js @@ -381,8 +381,8 @@ export default { }, paymentMethodList: { addPaymentMethod: 'Agrega método de pago', - addDebitCard: 'Agregar tarjeta de débito', - addBankAccount: 'Agregar cuenta de banco', + addNewDebitCard: 'Agregar nueva tarjeta de débito', + addNewBankAccount: 'Agregar nueva cuenta de banco', accountLastFour: 'Cuenta con terminación', cardLastFour: 'Tarjeta con terminacíon', addFirstPaymentMethod: 'Añade un método de pago para enviar y recibir pagos directamente desde la aplicación.', @@ -432,6 +432,7 @@ export default { twoFactorCode: 'Autenticación de 2 factores', requiredWhen2FAEnabled: 'Obligatorio cuando A2F está habilitado', error: { + incorrectPassword: 'Contraseña incorrecta. Por favor inténtalo de nuevo.', incorrectLoginOrPassword: 'Usuario o clave incorrectos. Por favor inténtalo de nuevo', twoFactorAuthenticationEnabled: 'Tienes autenticación de 2 factores activada en esta cuenta. Por favor conéctate usando su email o número de teléfono', invalidLoginOrPassword: 'Usuario o clave incorrectos. Por favor inténtalo de nuevo o resetea tu clave', @@ -478,8 +479,9 @@ export default { setPassword: 'Configura tu contraseña', newPasswordPrompt: 'La contraseña debe tener al menos 8 caracteres, 1 letra mayúscula, 1 letra minúscula y 1 número.', passwordFormTitle: '¡Bienvenido de vuelta al Nuevo Expensify! Por favor, elige una contraseña.', - passwordNotSet: 'No pudimos establecer to contaseña correctamente.', - accountNotValidated: 'No pudimos validar tu cuenta. Es posible que el enlace de validación haya caducado.', + passwordNotSet: 'No pudimos cambiar tu clave. Te hemos enviado un nuevo enlace para que intentes cambiar la clave nuevamente.', + setPasswordLinkInvalid: 'El enlace para configurar tu contraseña ha expirado. Te hemos enviado un nuevo enlace a tu correo.', + verifyingAccount: 'Verificando cuenta', }, stepCounter: ({step, total}) => `Paso ${step} de ${total}`, bankAccount: { @@ -888,11 +890,14 @@ export default { restrictedDescription: 'Sólo las personas en tu espacio de trabajo pueden encontrar esta sala a través de "Buscar"', privateDescription: 'Sólo las personas que están invitadas a esta sala pueden encontrarla', createRoom: 'Crea una sala de chat', + policyRoomRenamed: '¡Espacio de trabajo renombrado!', roomAlreadyExistsError: 'Ya existe una sala con este nombre', roomNameReservedError: 'Este nombre está reservado y no puede usarse', + renamedRoomAction: ({oldName, newName}) => ` cambió el nombre de la sala de ${oldName} a ${newName}`, social: 'social', selectAWorkspace: 'Seleccionar un espacio de trabajo', growlMessageOnError: 'No ha sido posible crear el espacio de trabajo, por favor comprueba tu conexión e inténtalo de nuevo.', + growlMessageOnRenameError: 'No ha sido posible cambiar el nomdre del espacio de trabajo, por favor comprueba tu conexión e inténtalo de nuevo.', visibilityOptions: { restricted: 'Restringida', private: 'Privada', diff --git a/src/libs/API.js b/src/libs/API.js index 7542f9690984..22b205dfbd0e 100644 --- a/src/libs/API.js +++ b/src/libs/API.js @@ -359,8 +359,8 @@ function AddBillingCard(parameters) { /** - * @param {Object} parameters - * @param {String} parameters.oldPassword + * @param {{password: String, oldPassword: String}} parameters + * @param {String} parameters.authToken * @param {String} parameters.password * @returns {Promise} */ @@ -727,7 +727,7 @@ function SetNameValuePair(parameters) { /** * @param {Object} parameters - * @param {Number} parameters.email + * @param {string} parameters.email * @returns {Promise} */ function ResetPassword(parameters) { @@ -740,7 +740,7 @@ function ResetPassword(parameters) { * @param {Object} parameters * @param {String} parameters.password * @param {String} parameters.validateCode - * @param {String} parameters.accountID + * @param {Number} parameters.accountID * @returns {Promise} */ function SetPassword(parameters) { @@ -1167,6 +1167,19 @@ function CreatePolicyRoom(parameters) { return Network.post(commandName, parameters); } +/** + * Renames a user-created policy room + * @param {Object} parameters + * @param {String} parameters.reportID + * @param {String} parameters.reportName + * @return {Promise} + */ +function RenameReport(parameters) { + const commandName = 'RenameReport'; + requireParameters(['reportID', 'reportName'], parameters, commandName); + return Network.post(commandName, parameters); +} + /** * Transfer Wallet balance and takes either the bankAccoundID or fundID * @param {Object} parameters @@ -1194,6 +1207,7 @@ export { CreateChatReport, CreateLogin, CreatePolicyRoom, + RenameReport, DeleteFund, DeleteLogin, DeleteBankAccount, diff --git a/src/libs/OptionsListUtils.js b/src/libs/OptionsListUtils.js index 4dc9af374968..17984bdbe36e 100644 --- a/src/libs/OptionsListUtils.js +++ b/src/libs/OptionsListUtils.js @@ -395,7 +395,8 @@ function getOptions(reports, personalDetails, activeReportID, { const logins = lodashGet(report, ['participants'], []); // Report data can sometimes be incomplete. If we have no logins or reportID then we will skip this entry. - if (!report || !report.reportID || (_.isEmpty(logins) && !ReportUtils.isChatRoom(report))) { + const shouldFilterNoParticipants = _.isEmpty(logins) && !ReportUtils.isChatRoom(report) && !ReportUtils.isDefaultRoom(report); + if (!report || !report.reportID || shouldFilterNoParticipants) { return; } @@ -405,7 +406,7 @@ function getOptions(reports, personalDetails, activeReportID, { : ''; const reportContainsIOUDebt = iouReportOwner && iouReportOwner !== currentUserLogin; - const shouldFilterReportIfEmpty = !showReportsWithNoComments && report.lastMessageTimestamp === 0; + const shouldFilterReportIfEmpty = !showReportsWithNoComments && report.lastMessageTimestamp === 0 && !ReportUtils.isDefaultRoom(report); const shouldFilterReportIfRead = hideReadReports && report.unreadActionCount === 0; const shouldFilterReport = shouldFilterReportIfEmpty || shouldFilterReportIfRead; if (report.reportID !== activeReportID diff --git a/src/libs/PaymentUtils.js b/src/libs/PaymentUtils.js index eb55234ece6f..674bffc9b8c5 100644 --- a/src/libs/PaymentUtils.js +++ b/src/libs/PaymentUtils.js @@ -65,7 +65,7 @@ function formatPaymentMethods(bankAccountList, cardList, payPalMeUsername = '', combinedPaymentMethods.push({ title: card.addressName, description: formattedCardNumber, - methodID: card.cardNumber, + methodID: card.fundID, icon, iconSize, key: `card-${card.cardNumber}`, diff --git a/src/libs/actions/PaymentMethods.js b/src/libs/actions/PaymentMethods.js index 4cfcc941ee8a..94a1b6cedb6f 100644 --- a/src/libs/actions/PaymentMethods.js +++ b/src/libs/actions/PaymentMethods.js @@ -184,10 +184,10 @@ function clearDebitCardFormErrorAndSubmit() { * Call the API to transfer wallet balance. * @param {Object} paymentMethod * @param {*} paymentMethod.methodID - * @param {String} paymentMethod.type + * @param {String} paymentMethod.accountType */ function transferWalletBalance(paymentMethod) { - const paymentMethodIDKey = paymentMethod.type === CONST.PAYMENT_METHODS.BANK_ACCOUNT + const paymentMethodIDKey = paymentMethod.accountType === CONST.PAYMENT_METHODS.BANK_ACCOUNT ? CONST.PAYMENT_METHOD_ID_KEYS.BANK_ACCOUNT : CONST.PAYMENT_METHOD_ID_KEYS.DEBIT_CARD; const parameters = { @@ -209,13 +209,10 @@ function transferWalletBalance(paymentMethod) { }); } -/** - * Set the transfer account and reset the transfer data for Wallet balance transfer - * @param {String} selectedAccountID - */ -function saveWalletTransferAccountAndResetData(selectedAccountID) { +function resetWalletTransferData() { Onyx.merge(ONYXKEYS.WALLET_TRANSFER, { - selectedAccountID, + selectedAccountType: '', + selectedAccountID: null, filterPaymentMethodType: null, loading: false, shouldShowConfirmModal: false, @@ -229,6 +226,22 @@ function saveWalletTransferAmount(transferAmount) { Onyx.merge(ONYXKEYS.WALLET_TRANSFER, {transferAmount}); } +/** + * @param {String} selectedAccountType + * @param {String} selectedAccountID + */ +function saveWalletTransferAccountTypeAndID(selectedAccountType, selectedAccountID) { + Onyx.merge(ONYXKEYS.WALLET_TRANSFER, {selectedAccountType, selectedAccountID}); +} + +/** + * Toggles the user's selected type of payment method (bank account or debit card) on the wallet transfer balance screen. + * @param {String} filterPaymentMethodType + */ +function saveWalletTransferMethodType(filterPaymentMethodType) { + Onyx.merge(ONYXKEYS.WALLET_TRANSFER, {filterPaymentMethodType}); +} + function dismissWalletConfirmModal() { Onyx.merge(ONYXKEYS.WALLET_TRANSFER, {shouldShowConfirmModal: false}); } @@ -243,7 +256,9 @@ export { continueSetup, clearDebitCardFormErrorAndSubmit, transferWalletBalance, - saveWalletTransferAccountAndResetData, + resetWalletTransferData, saveWalletTransferAmount, + saveWalletTransferAccountTypeAndID, + saveWalletTransferMethodType, dismissWalletConfirmModal, }; diff --git a/src/libs/actions/PersonalDetails.js b/src/libs/actions/PersonalDetails.js index 22453fcd4ee9..aaeb85f953d9 100644 --- a/src/libs/actions/PersonalDetails.js +++ b/src/libs/actions/PersonalDetails.js @@ -8,7 +8,6 @@ import CONST from '../../CONST'; import NetworkConnection from '../NetworkConnection'; import * as API from '../API'; import NameValuePair from './NameValuePair'; -import * as ReportUtils from '../reportUtils'; import * as OptionsListUtils from '../OptionsListUtils'; import Growl from '../Growl'; import * as Localize from '../Localize'; @@ -146,6 +145,7 @@ function fetchPersonalDetails() { * Get personal details from report participants. * * @param {Object} reports + * @returns {Promise} */ function getFromReportParticipants(reports) { const participantEmails = _.chain(reports) @@ -155,10 +155,10 @@ function getFromReportParticipants(reports) { .value(); if (participantEmails.length === 0) { - return; + return Promise.resolve({}); } - API.PersonalDetails_GetForEmails({emailList: participantEmails.join(',')}) + return API.PersonalDetails_GetForEmails({emailList: participantEmails.join(',')}) .then((data) => { const existingDetails = _.pick(data, participantEmails); @@ -173,36 +173,7 @@ function getFromReportParticipants(reports) { const formattedPersonalDetails = formatPersonalDetails(details); Onyx.merge(ONYXKEYS.PERSONAL_DETAILS, formattedPersonalDetails); - - // The personalDetails of the participants contain their avatar images. Here we'll go over each - // report and based on the participants we'll link up their avatars to report icons. This will - // skip over default rooms which aren't named by participants. - const reportsToUpdate = {}; - _.each(reports, (report) => { - if (report.participants.length <= 0 && !ReportUtils.isChatRoom(report)) { - return; - } - - const avatars = OptionsListUtils.getReportIcons(report, details); - const reportName = ReportUtils.isChatRoom(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. - // Any withOnyx subscribers to this key will also receive the complete updated props just once - // than updating props for each report and re-rendering had merge been used. - Onyx.mergeCollection(ONYXKEYS.COLLECTION.REPORT, reportsToUpdate); + return details; }); } diff --git a/src/libs/actions/Policy.js b/src/libs/actions/Policy.js index f71a681b24ce..5dd0dd627051 100644 --- a/src/libs/actions/Policy.js +++ b/src/libs/actions/Policy.js @@ -10,6 +10,7 @@ import * as Localize from '../Localize'; import Navigation from '../Navigation/Navigation'; import ROUTES from '../../ROUTES'; import * as OptionsListUtils from '../OptionsListUtils'; +import * as Report from './Report'; const allPolicies = {}; Onyx.connect({ @@ -111,6 +112,9 @@ function create(name = '') { } res = response; + // Fetch the default reports on the policy + Report.fetchChatReportsByIDs([response.policy.chatReportIDAdmins, response.policy.chatReportIDAnnounce]); + // We are awaiting this merge so that we can guarantee our policy is available to any React components connected to the policies collection before we navigate to a new route. return Onyx.merge(`${ONYXKEYS.COLLECTION.POLICY}${response.policyID}`, { employeeList: getSimplifiedEmployeeList(response.policy.employeeList), @@ -158,7 +162,9 @@ function deletePolicy(policyID) { // Removing the workspace data from Onyx as well return Onyx.set(`${ONYXKEYS.COLLECTION.POLICY}${policyID}`, null); - }).then(() => { + }) + .then(() => Report.fetchAllReports(false, true)) + .then(() => { Navigation.dismissModal(); Navigation.navigate(ROUTES.HOME); return Promise.resolve(); diff --git a/src/libs/actions/Report.js b/src/libs/actions/Report.js index 0519db956ad4..5dd955510b08 100644 --- a/src/libs/actions/Report.js +++ b/src/libs/actions/Report.js @@ -10,6 +10,7 @@ import * as Pusher from '../Pusher/pusher'; import LocalNotification from '../Notification/LocalNotification'; import PushNotification from '../Notification/PushNotification'; import * as PersonalDetails from './PersonalDetails'; +import * as User from './User'; import Navigation from '../Navigation/Navigation'; import * as ActiveClientManager from '../ActiveClientManager'; import Visibility from '../Visibility'; @@ -24,6 +25,7 @@ import Timers from '../Timers'; import * as ReportActions from './ReportActions'; import Growl from '../Growl'; import * as Localize from '../Localize'; +import * as OptionsListUtils from '../OptionsListUtils'; let currentUserEmail; let currentUserAccountID; @@ -318,6 +320,39 @@ function fetchIOUReportID(debtorEmail) { }); } +function configureReportNameAndIcon(reports, details) { + // The personalDetails of the participants contain their avatar images. Here we'll go over each + // report and based on the participants we'll link up their avatars to report icons. This will + // skip over default rooms which aren't named by participants. + + const reportsToUpdate = {}; + _.each(reports, (report) => { + if (report.participants.length <= 0 && !ReportUtils.isChatRoom(report)) { + return; + } + + const avatars = ReportUtils.isChatRoom(report) ? (['']) : OptionsListUtils.getReportIcons(report, details); + const reportName = ReportUtils.isChatRoom(report) + ? report.reportName + : _.chain(report.participants) + .filter(participant => participant !== currentUserEmail) + .map(participant => lodashGet( + details, + [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. + // Any withOnyx subscribers to this key will also receive the complete updated props just once + // than updating props for each report and re-rendering had merge been used. + Onyx.mergeCollection(ONYXKEYS.COLLECTION.REPORT, reportsToUpdate); +} + /** * Fetches chat reports when provided a list of chat report IDs. * If the shouldRedirectIfInaccessible flag is set, we redirect to the Concierge chat @@ -398,8 +433,12 @@ function fetchChatReportsByIDs(chatList, shouldRedirectIfInaccessible = false) { Onyx.mergeCollection(ONYXKEYS.COLLECTION.REPORT_IOUS, reportIOUData); Onyx.mergeCollection(ONYXKEYS.COLLECTION.REPORT, simplifiedReports); + const simplifiedReportsList = _.values(simplifiedReports); + // Fetch the personal details if there are any - PersonalDetails.getFromReportParticipants(_.values(simplifiedReports)); + PersonalDetails.getFromReportParticipants(simplifiedReportsList) + .then(details => configureReportNameAndIcon(simplifiedReportsList, details)); + return fetchedReports; }) .catch((err) => { @@ -584,7 +623,10 @@ function updateReportWithNewAction( setLocalLastRead(reportID, newMaxSequenceNumber); } - const messageText = lodashGet(reportAction, ['message', 0, 'text'], ''); + let messageText = lodashGet(reportAction, ['message', 0, 'text'], ''); + if (reportAction.actionName === CONST.REPORT.ACTIONS.TYPE.RENAMED) { + messageText = lodashGet(reportAction, 'originalMessage.html', ''); + } // Always merge the reportID into Onyx // If the report doesn't exist in Onyx yet, then all the rest of the data will be filled out @@ -1181,6 +1223,18 @@ function addAction(reportID, text, file) { console.error(response.message); return; } + + if (response.jsonCode === 666 && reportID === conciergeChatReportID) { + Growl.error(Localize.translateLocal('reportActionCompose.blockedFromConcierge')); + Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`, { + [optimisticReportActionID]: null, + }); + + // The fact that the API is returning this error means the BLOCKED_FROM_CONCIERGE nvp in the user details has changed since the last time we checked, so let's update + User.getUserDetails(); + return; + } + updateReportWithNewAction(reportID, response.reportAction); }); } @@ -1530,6 +1584,30 @@ function createPolicyRoom(policyID, reportName, visibility) { .finally(() => Onyx.set(ONYXKEYS.IS_LOADING_CREATE_POLICY_ROOM, false)); } +/** + * Renames a user created Policy Room. + * @param {String} reportID + * @param {String} reportName + */ +function renameReport(reportID, reportName) { + Onyx.set(ONYXKEYS.IS_LOADING_RENAME_POLICY_ROOM, true); + API.RenameReport({reportID, reportName}) + .then((response) => { + if (response.jsonCode !== 200) { + Growl.error(response.message); + return; + } + Growl.success(Localize.translateLocal('newRoomPage.policyRoomRenamed')); + + // Update the report name so that the LHN and header display the updated name + Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT}${reportID}`, {reportName}); + }) + .catch(() => { + Growl.error(Localize.translateLocal('newRoomPage.growlMessageOnRenameError')); + }) + .finally(() => Onyx.set(ONYXKEYS.IS_LOADING_RENAME_POLICY_ROOM, false)); +} + export { fetchAllReports, fetchActions, @@ -1559,5 +1637,6 @@ export { setReportWithDraft, fetchActionsWithLoadingState, createPolicyRoom, + renameReport, getLastReadSequenceNumber, }; diff --git a/src/libs/actions/Session/index.js b/src/libs/actions/Session/index.js index 245ba1c37395..490a7951648a 100644 --- a/src/libs/actions/Session/index.js +++ b/src/libs/actions/Session/index.js @@ -293,11 +293,10 @@ function resetPassword() { * * @param {String} password * @param {String} validateCode - * @param {String} accountID + * @param {Number} accountID */ function setPassword(password, validateCode, accountID) { Onyx.merge(ONYXKEYS.ACCOUNT, {...CONST.DEFAULT_ACCOUNT_DATA, loading: true, validateCodeExpired: false}); - API.SetPassword({ password, validateCode, @@ -384,52 +383,84 @@ function clearAccountMessages() { } /** + * Calls change password and signs if if successful. Otherwise, we request a new magic link + * if we know the account email. Otherwise or finally we redirect to the root of the nav. * @param {String} authToken * @param {String} password */ function changePasswordAndSignIn(authToken, password) { + Onyx.merge(ONYXKEYS.ACCOUNT, {validateSessionExpired: false}); API.ChangePassword({ authToken, password, }) .then((responsePassword) => { + Onyx.merge(ONYXKEYS.USER_SIGN_UP, {authToken: null}); if (responsePassword.jsonCode === 200) { signIn(password); return; } - + if (responsePassword.jsonCode === 407 && !credentials.login) { + // authToken has expired, and we don't have the email set to request a new magic link. + // send user to login page to enter email. + Navigation.navigate(ROUTES.HOME); + return; + } + if (responsePassword.jsonCode === 407) { + // authToken has expired, and we have the account email, so we request a new magic link. + Onyx.merge(ONYXKEYS.ACCOUNT, {accountExists: true, validateCodeExpired: true, error: null}); + resetPassword(); + Navigation.navigate(ROUTES.HOME); + return; + } Onyx.merge(ONYXKEYS.SESSION, {error: 'setPasswordPage.passwordNotSet'}); }); } /** - * @param {String} accountID + * Call set or change password based on if we have an auth token + * @param {Number} accountID * @param {String} validateCode * @param {String} password + * @param {String} authToken */ -function validateEmail(accountID, validateCode, password) { + +function setOrChangePassword(accountID, validateCode, password, authToken) { + if (authToken) { + changePasswordAndSignIn(authToken, password); + return; + } + setPassword(password, validateCode, accountID); +} + +/** + * @param {Number} accountID + * @param {String} validateCode + * @param {String} login + * @param {String} authToken + */ +function validateEmail(accountID, validateCode) { + Onyx.merge(ONYXKEYS.USER_SIGN_UP, {isValidating: true}); + Onyx.merge(ONYXKEYS.SESSION, {error: ''}); API.ValidateEmail({ accountID, validateCode, }) .then((responseValidate) => { if (responseValidate.jsonCode === 200) { - changePasswordAndSignIn(responseValidate.authToken, password); + Onyx.merge(ONYXKEYS.USER_SIGN_UP, {authToken: responseValidate.authToken}); + Onyx.merge(ONYXKEYS.ACCOUNT, {accountExists: true, validated: true}); + Onyx.merge(ONYXKEYS.CREDENTIALS, {login: responseValidate.email}); return; } - - if (responseValidate.title === CONST.PASSWORD_PAGE.ERROR.ALREADY_VALIDATED) { - // If the email is already validated, set the password using the validate code - setPassword( - password, - validateCode, - accountID, - ); - return; + if (responseValidate.jsonCode === 666) { + Onyx.merge(ONYXKEYS.ACCOUNT, {accountExists: true, validated: true}); } - - Onyx.merge(ONYXKEYS.SESSION, {error: 'setPasswordPage.accountNotValidated'}); - }); + if (responseValidate.jsonCode === 401) { + Onyx.merge(ONYXKEYS.SESSION, {error: 'setPasswordPage.setPasswordLinkInvalid'}); + } + }) + .finally(Onyx.merge(ONYXKEYS.USER_SIGN_UP, {isValidating: false})); } // It's necessary to throttle requests to reauthenticate since calling this multiple times will cause Pusher to @@ -493,6 +524,7 @@ function setShouldShowComposeInput(shouldShowComposeInput) { export { continueSessionFromECom, fetchAccountDetails, + setOrChangePassword, setPassword, signIn, signInWithShortLivedToken, @@ -507,4 +539,5 @@ export { authenticatePusher, reauthenticatePusher, setShouldShowComposeInput, + changePasswordAndSignIn, }; diff --git a/src/libs/actions/User.js b/src/libs/actions/User.js index 455812491ee2..b3ec5411b526 100644 --- a/src/libs/actions/User.js +++ b/src/libs/actions/User.js @@ -103,6 +103,7 @@ function getUserDetails() { CONST.NVP.PAYPAL_ME_ADDRESS, CONST.NVP.PREFERRED_EMOJI_SKIN_TONE, CONST.NVP.FREQUENTLY_USED_EMOJIS, + CONST.NVP.BLOCKED_FROM_CONCIERGE, ].join(','), }) .then((response) => { diff --git a/src/libs/virtualKeyboard/index.js b/src/libs/virtualKeyboard/index.js new file mode 100644 index 000000000000..f28dc84c0bcf --- /dev/null +++ b/src/libs/virtualKeyboard/index.js @@ -0,0 +1,18 @@ +import _ from 'underscore'; + +/** + * Is the virtual keyboard open? + * + * @returns {Boolean|null} – null if the VirtualKeyboard API is unavailable + */ +function isOpen() { + if (!_.has(navigator, 'virtualKeyboard')) { + return null; + } + return navigator.virtualKeyboard.boundingRect.y > 0; +} + +export { + // eslint-disable-next-line import/prefer-default-export + isOpen, +}; diff --git a/src/libs/virtualKeyboard/index.native.js b/src/libs/virtualKeyboard/index.native.js new file mode 100644 index 000000000000..6c21a32ffcdf --- /dev/null +++ b/src/libs/virtualKeyboard/index.native.js @@ -0,0 +1,33 @@ +import {Keyboard} from 'react-native'; + +let isVirtualKeyboardOpen = false; + +Keyboard.addListener( + 'keyboardDidShow', + () => { + isVirtualKeyboardOpen = true; + }, +); + +Keyboard.addListener( + 'keyboardDidHide', + () => { + isVirtualKeyboardOpen = false; + }, +); + +/** + * Is the virtual keyboard open? + * + * Note – the web equivalent of this function may return null. + * + * @returns {Boolean} + */ +function isOpen() { + return isVirtualKeyboardOpen; +} + +export { + // eslint-disable-next-line import/prefer-default-export + isOpen, +}; diff --git a/src/pages/ReportSettingsPage.js b/src/pages/ReportSettingsPage.js index 1543d8686d1c..ac3e914988ca 100644 --- a/src/pages/ReportSettingsPage.js +++ b/src/pages/ReportSettingsPage.js @@ -89,7 +89,7 @@ class ReportSettingsPage extends Component { } render() { - const shouldDisableRename = ReportUtils.isDefaultRoom(this.props.report) || ReportUtils.isArchivedRoom(this.props.report); + const shouldDisableRename = ReportUtils.isDefaultRoom(this.props.report) || ReportUtils.isArchivedRoom(this.props.report) || this.props.isLoadingRenamePolicyRoom; const linkedWorkspace = _.find(this.props.policies, policy => policy.id === this.props.report.policyID); return ( @@ -141,10 +141,8 @@ class ReportSettingsPage extends Component {