diff --git a/android/app/build.gradle b/android/app/build.gradle index 04d66b326184..f957ea7c5854 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -90,8 +90,8 @@ android { minSdkVersion rootProject.ext.minSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion multiDexEnabled rootProject.ext.multiDexEnabled - versionCode 1001037102 - versionName "1.3.71-2" + versionCode 1001037105 + versionName "1.3.71-5" } flavorDimensions "default" diff --git a/ios/NewExpensify/Info.plist b/ios/NewExpensify/Info.plist index e0d3b2454561..07e1cfae80ac 100644 --- a/ios/NewExpensify/Info.plist +++ b/ios/NewExpensify/Info.plist @@ -40,7 +40,7 @@ CFBundleVersion - 1.3.71.2 + 1.3.71.5 ITSAppUsesNonExemptEncryption LSApplicationQueriesSchemes diff --git a/ios/NewExpensifyTests/Info.plist b/ios/NewExpensifyTests/Info.plist index a5de0b39bdff..49d12cf93594 100644 --- a/ios/NewExpensifyTests/Info.plist +++ b/ios/NewExpensifyTests/Info.plist @@ -19,6 +19,6 @@ CFBundleSignature ???? CFBundleVersion - 1.3.71.2 + 1.3.71.5 diff --git a/package-lock.json b/package-lock.json index 4b42a8a292e3..68ea17fc20b8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "new.expensify", - "version": "1.3.71-2", + "version": "1.3.71-5", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "new.expensify", - "version": "1.3.71-2", + "version": "1.3.71-5", "hasInstallScript": true, "license": "MIT", "dependencies": { @@ -69,6 +69,7 @@ "react-collapse": "^5.1.0", "react-content-loader": "^6.1.0", "react-dom": "18.1.0", + "react-error-boundary": "^4.0.11", "react-map-gl": "^7.1.3", "react-native": "0.72.3", "react-native-blob-util": "^0.17.3", @@ -40187,6 +40188,17 @@ "react": "^18.1.0" } }, + "node_modules/react-error-boundary": { + "version": "4.0.11", + "resolved": "https://registry.npmjs.org/react-error-boundary/-/react-error-boundary-4.0.11.tgz", + "integrity": "sha512-U13ul67aP5DOSPNSCWQ/eO0AQEYzEFkVljULQIjMV0KlffTAhxuDoBKdO0pb/JZ8mDhMKFZ9NZi0BmLGUiNphw==", + "dependencies": { + "@babel/runtime": "^7.12.5" + }, + "peerDependencies": { + "react": ">=16.13.1" + } + }, "node_modules/react-freeze": { "version": "1.0.3", "license": "MIT", @@ -75742,6 +75754,14 @@ "scheduler": "^0.22.0" } }, + "react-error-boundary": { + "version": "4.0.11", + "resolved": "https://registry.npmjs.org/react-error-boundary/-/react-error-boundary-4.0.11.tgz", + "integrity": "sha512-U13ul67aP5DOSPNSCWQ/eO0AQEYzEFkVljULQIjMV0KlffTAhxuDoBKdO0pb/JZ8mDhMKFZ9NZi0BmLGUiNphw==", + "requires": { + "@babel/runtime": "^7.12.5" + } + }, "react-freeze": { "version": "1.0.3", "requires": {} diff --git a/package.json b/package.json index 6b921644470b..72574455719b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "new.expensify", - "version": "1.3.71-2", + "version": "1.3.71-5", "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.", @@ -112,6 +112,7 @@ "react-content-loader": "^6.1.0", "react-dom": "18.1.0", "react-map-gl": "^7.1.3", + "react-error-boundary": "^4.0.11", "react-native": "0.72.3", "react-native-blob-util": "^0.17.3", "react-native-collapsible": "^1.6.0", diff --git a/src/components/AvatarWithImagePicker.js b/src/components/AvatarWithImagePicker.js index aa50d6db574b..3a43ede5a8f4 100644 --- a/src/components/AvatarWithImagePicker.js +++ b/src/components/AvatarWithImagePicker.js @@ -23,6 +23,8 @@ import getImageResolution from '../libs/fileDownload/getImageResolution'; import PressableWithoutFeedback from './Pressable/PressableWithoutFeedback'; import DotIndicatorMessage from './DotIndicatorMessage'; import * as Browser from '../libs/Browser'; +import withNavigationFocus, {withNavigationFocusPropTypes} from './withNavigationFocus'; +import compose from '../libs/compose'; const propTypes = { /** Avatar source to display */ @@ -80,6 +82,7 @@ const propTypes = { errors: PropTypes.object, ...withLocalizePropTypes, + ...withNavigationFocusPropTypes, }; const defaultProps = { @@ -129,6 +132,9 @@ class AvatarWithImagePicker extends React.Component { } componentDidUpdate(prevProps) { + if (!prevProps.isFocused && this.props.isFocused) { + this.setError(null, {}); + } if (!prevProps.isUploading && this.props.isUploading) { this.animation.start(); } else if (prevProps.isUploading && !this.props.isUploading) { @@ -350,4 +356,4 @@ class AvatarWithImagePicker extends React.Component { AvatarWithImagePicker.propTypes = propTypes; AvatarWithImagePicker.defaultProps = defaultProps; -export default withLocalize(AvatarWithImagePicker); +export default compose(withLocalize, withNavigationFocus)(AvatarWithImagePicker); diff --git a/src/components/ErrorBoundary/BaseErrorBoundary.js b/src/components/ErrorBoundary/BaseErrorBoundary.js index e479b04f7ade..d626442e81dd 100644 --- a/src/components/ErrorBoundary/BaseErrorBoundary.js +++ b/src/components/ErrorBoundary/BaseErrorBoundary.js @@ -1,5 +1,6 @@ import React from 'react'; import PropTypes from 'prop-types'; +import {ErrorBoundary} from 'react-error-boundary'; import BootSplash from '../../libs/BootSplash'; import GenericErrorPage from '../../pages/ErrorPage/GenericErrorPage'; @@ -22,40 +23,27 @@ const defaultProps = { * This component captures an error in the child component tree and logs it to the server * It can be used to wrap the entire app as well as to wrap specific parts for more granularity * @see {@link https://reactjs.org/docs/error-boundaries.html#where-to-place-error-boundaries} + * @return {React.Component} */ -class BaseErrorBoundary extends React.Component { - constructor(props) { - super(props); - this.state = {hasError: false}; - this.clearError = this.clearError.bind(this); - } - - static getDerivedStateFromError() { - // Update state so the next render will show the fallback UI. - return {hasError: true}; - } - - componentDidCatch(error, errorInfo) { - this.props.logError(this.props.errorMessage, error, JSON.stringify(errorInfo)); - +function BaseErrorBoundary({logError, errorMessage, children}) { + const catchError = (error, errorInfo) => { + logError(errorMessage, error, JSON.stringify(errorInfo)); // We hide the splash screen since the error might happened during app init BootSplash.hide(); - } - - clearError() { - this.setState({hasError: false}); - } - - render() { - if (this.state.hasError) { - return ; - } - - return this.props.children; - } + }; + + return ( + } + onError={catchError} + > + {children} + + ); } BaseErrorBoundary.propTypes = propTypes; BaseErrorBoundary.defaultProps = defaultProps; +BaseErrorBoundary.displayName = 'BaseErrorBoundary'; export default BaseErrorBoundary; diff --git a/src/components/MoneyRequestConfirmationList.js b/src/components/MoneyRequestConfirmationList.js index cba2359eb257..6448a62639dc 100755 --- a/src/components/MoneyRequestConfirmationList.js +++ b/src/components/MoneyRequestConfirmationList.js @@ -5,6 +5,7 @@ import {format} from 'date-fns'; import _ from 'underscore'; import {View} from 'react-native'; import lodashGet from 'lodash/get'; +import Text from './Text'; import styles from '../styles/styles'; import * as ReportUtils from '../libs/ReportUtils'; import * as OptionsListUtils from '../libs/OptionsListUtils'; @@ -30,6 +31,7 @@ import Image from './Image'; import useLocalize from '../hooks/useLocalize'; import * as ReceiptUtils from '../libs/ReceiptUtils'; import categoryPropTypes from './categoryPropTypes'; +import Switch from './Switch'; import tagPropTypes from './tagPropTypes'; import ConfirmedRoute from './ConfirmedRoute'; import transactionPropTypes from './transactionPropTypes'; @@ -531,6 +533,16 @@ function MoneyRequestConfirmationList(props) { disabled={didConfirm || props.isReadOnly} /> )} + {canUseTags && !lodashGet(props.policy, 'disabledFields.defaultBillable', true) && ( + + {translate('common.billable')} + + + )} )} @@ -562,5 +574,8 @@ export default compose( transaction: { key: ({transactionID}) => `${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`, }, + policy: { + key: ({policyID}) => `${ONYXKEYS.COLLECTION.POLICY}${policyID}`, + }, }), )(MoneyRequestConfirmationList); diff --git a/src/components/ReportActionItem/TaskPreview.js b/src/components/ReportActionItem/TaskPreview.js index ca4103624440..d0181e3d736a 100644 --- a/src/components/ReportActionItem/TaskPreview.js +++ b/src/components/ReportActionItem/TaskPreview.js @@ -72,6 +72,11 @@ function TaskPreview(props) { const assigneeDisplayName = lodashGet(props.personalDetailsList, [taskAssigneeAccountID, 'displayName'], ''); const taskAssignee = assigneeLogin || assigneeDisplayName; const htmlForTaskPreview = taskAssignee ? `@${taskAssignee} ${taskTitle}` : `${taskTitle}`; + const isDeletedParentAction = ReportUtils.isCanceledTaskReport(props.taskReport, props.action); + + if (isDeletedParentAction) { + return ${props.translate('parentReportAction.deletedTask')}`} />; + } return ( diff --git a/src/languages/en.ts b/src/languages/en.ts index c31e39319a98..25c31ac194d9 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -244,6 +244,7 @@ export default { showMore: 'Show more', merchant: 'Merchant', category: 'Category', + billable: 'Billable', tag: 'Tag', receipt: 'Receipt', replace: 'Replace', @@ -1524,7 +1525,7 @@ export default { completed: 'Completed', messages: { completed: 'completed task', - canceled: 'canceled task', + canceled: 'deleted task', reopened: 'reopened task', error: 'You do not have the permission to do the requested action.', }, @@ -1696,6 +1697,7 @@ export default { parentReportAction: { deletedMessage: '[Deleted message]', deletedRequest: '[Deleted request]', + deletedTask: '[Deleted task]', hiddenMessage: '[Hidden message]', }, threads: { diff --git a/src/languages/es.ts b/src/languages/es.ts index d83104ff85e0..0f4e5807cdd7 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -234,6 +234,7 @@ export default { showMore: 'Mostrar más', merchant: 'Comerciante', category: 'Categoría', + billable: 'Facturable', tag: 'Etiqueta', receipt: 'Recibo', replace: 'Sustituir', @@ -1546,7 +1547,7 @@ export default { completed: 'Completada', messages: { completed: 'tarea completada', - canceled: 'tarea cancelada', + canceled: 'tarea eliminado', reopened: 'tarea reabrir', error: 'No tiene permiso para realizar la acción solicitada.', }, @@ -2178,6 +2179,7 @@ export default { parentReportAction: { deletedMessage: '[Mensaje eliminado]', deletedRequest: '[Pedido eliminado]', + deletedTask: '[Tarea eliminado]', hiddenMessage: '[Mensaje oculto]', }, threads: { diff --git a/src/libs/LocalePhoneNumber.js b/src/libs/LocalePhoneNumber.ts similarity index 82% rename from src/libs/LocalePhoneNumber.js rename to src/libs/LocalePhoneNumber.ts index e5c7cbfa45ba..962040aee049 100644 --- a/src/libs/LocalePhoneNumber.js +++ b/src/libs/LocalePhoneNumber.ts @@ -3,20 +3,17 @@ import Str from 'expensify-common/lib/str'; import {parsePhoneNumber} from 'awesome-phonenumber'; import ONYXKEYS from '../ONYXKEYS'; -let countryCodeByIP; +let countryCodeByIP: number; Onyx.connect({ key: ONYXKEYS.COUNTRY_CODE, - callback: (val) => (countryCodeByIP = val || 1), + callback: (val) => (countryCodeByIP = val ?? 1), }); /** * Returns a locally converted phone number for numbers from the same region * and an internationally converted phone number with the country code for numbers from other regions - * - * @param {String} number - * @returns {String} */ -function formatPhoneNumber(number) { +function formatPhoneNumber(number: string): string { if (!number) { return ''; } @@ -26,7 +23,7 @@ function formatPhoneNumber(number) { // return the string untouched if it's not a phone number if (!parsedPhoneNumber.valid) { - if (parsedPhoneNumber.number && parsedPhoneNumber.number.international) { + if (parsedPhoneNumber.number?.international) { return parsedPhoneNumber.number.international; } return numberWithoutSMSDomain; diff --git a/src/libs/ReportUtils.js b/src/libs/ReportUtils.js index 9396ea921b61..7916630d7b4e 100644 --- a/src/libs/ReportUtils.js +++ b/src/libs/ReportUtils.js @@ -187,23 +187,37 @@ function isTaskReport(report) { } /** - * Checks if a report is an open task report. + * Checks if a task has been cancelled + * When a task is deleted, the parentReportAction is updated to have a isDeletedParentAction deleted flag + * This is because when you delete a task, we still allow you to chat on the report itself + * There's another situation where you don't have access to the parentReportAction (because it was created in a chat you don't have access to) + * In this case, we have added the key to the report itself * * @param {Object} report + * @param {Object} parentReportAction * @returns {Boolean} */ -function isOpenTaskReport(report) { - return isTaskReport(report) && report.stateNum === CONST.REPORT.STATE_NUM.OPEN && report.statusNum === CONST.REPORT.STATUS.OPEN; +function isCanceledTaskReport(report = {}, parentReportAction = {}) { + if (!_.isEmpty(parentReportAction) && lodashGet(parentReportAction, ['message', 0, 'isDeletedParentAction'], false)) { + return true; + } + + if (!_.isEmpty(report) && report.isDeletedParentAction) { + return true; + } + + return false; } /** - * Checks if the current user is assigned to the task report + * Checks if a report is an open task report. * * @param {Object} report + * @param {Object} parentReportAction - The parent report action of the report (Used to check if the task has been canceled) * @returns {Boolean} */ -function isCanceledTaskReport(report) { - return isTaskReport(report) && report.stateNum === CONST.REPORT.STATE_NUM.SUBMITTED && report.statusNum === CONST.REPORT.STATUS.CLOSED; +function isOpenTaskReport(report, parentReportAction = {}) { + return isTaskReport(report) && !isCanceledTaskReport(report, parentReportAction) && report.stateNum === CONST.REPORT.STATE_NUM.OPEN && report.statusNum === CONST.REPORT.STATUS.OPEN; } /** @@ -484,7 +498,7 @@ function shouldDisableDetailPage(report) { */ function isExpensifyOnlyParticipantInReport(report) { const reportParticipants = _.without(lodashGet(report, 'participantAccountIDs', []), currentUserAccountID); - return lodashGet(report, 'participantAccountIDs', []).length === 1 && _.some(reportParticipants, (accountID) => _.contains(CONST.EXPENSIFY_ACCOUNT_IDS, accountID)); + return reportParticipants.length === 1 && _.some(reportParticipants, (accountID) => _.contains(CONST.EXPENSIFY_ACCOUNT_IDS, accountID)); } /** @@ -1610,8 +1624,8 @@ function getParentReport(report) { */ function getReportName(report, policy = undefined) { let formattedName; + const parentReportAction = ReportActionsUtils.getParentReportAction(report); if (isChatThread(report)) { - const parentReportAction = ReportActionsUtils.getParentReportAction(report); if (ReportActionsUtils.isTransactionThread(parentReportAction)) { return getTransactionReportName(parentReportAction); } @@ -1629,6 +1643,11 @@ function getReportName(report, policy = undefined) { } return parentReportActionMessage || Localize.translateLocal('parentReportAction.deletedMessage'); } + + if (isTaskReport(report) && isCanceledTaskReport(report, parentReportAction)) { + return Localize.translateLocal('parentReportAction.deletedTask'); + } + if (isChatRoom(report) || isTaskReport(report)) { formattedName = report.reportName; } diff --git a/src/libs/TransactionUtils.js b/src/libs/TransactionUtils.js index dccabd74772b..c7553a22cb56 100644 --- a/src/libs/TransactionUtils.js +++ b/src/libs/TransactionUtils.js @@ -34,6 +34,7 @@ Onyx.connect({ * @param {String} [filename] * @param {String} [existingTransactionID] When creating a distance request, an empty transaction has already been created with a transactionID. In that case, the transaction here needs to have it's transactionID match what was already generated. * @param {String} [category] + * @param {Boolean} [billable] * @returns {Object} */ function buildOptimisticTransaction( @@ -49,6 +50,7 @@ function buildOptimisticTransaction( filename = '', existingTransactionID = null, category = '', + billable = false, ) { // transactionIDs are random, positive, 64-bit numeric strings. // Because JS can only handle 53-bit numbers, transactionIDs are strings in the front-end (just like reportActionID) @@ -77,6 +79,7 @@ function buildOptimisticTransaction( receipt, filename, category, + billable, }; } diff --git a/src/libs/actions/IOU.js b/src/libs/actions/IOU.js index 3cefcd00ed60..8bbcb58f71f4 100644 --- a/src/libs/actions/IOU.js +++ b/src/libs/actions/IOU.js @@ -98,6 +98,7 @@ function resetMoneyRequestInfo(id = '') { receiptPath: '', receiptSource: '', transactionID: '', + billable: null, }); } @@ -341,6 +342,7 @@ function buildOnyxDataForMoneyRequest( * @param {Object} [receipt] * @param {String} [existingTransactionID] * @param {String} [category] + * @param {Boolean} [billable] * @returns {Object} data * @returns {String} data.payerEmail * @returns {Object} data.iouReport @@ -368,6 +370,7 @@ function getMoneyRequestInformation( receipt = undefined, existingTransactionID = undefined, category = undefined, + billable = undefined, ) { const payerEmail = OptionsListUtils.addSMSDomainIfPhoneNumber(participant.login); const payerAccountID = Number(participant.accountID); @@ -433,6 +436,7 @@ function getMoneyRequestInformation( filename, existingTransactionID, category, + billable, ); const uniquePolicyRecentlyUsedCategories = allRecentlyUsedCategories @@ -600,8 +604,9 @@ function createDistanceRequest(report, participant, comment, created, transactio * @param {String} comment * @param {Object} [receipt] * @param {String} [category] + * @param {Boolean} [billable] */ -function requestMoney(report, amount, currency, created, merchant, payeeEmail, payeeAccountID, participant, comment, receipt = undefined, category = undefined) { +function requestMoney(report, amount, currency, created, merchant, payeeEmail, payeeAccountID, participant, comment, receipt = undefined, category = undefined, billable = undefined) { // If the report is iou or expense report, we should get the linked chat report to be passed to the getMoneyRequestInformation function const isMoneyRequestReport = ReportUtils.isMoneyRequestReport(report); const currentChatReport = isMoneyRequestReport ? ReportUtils.getReport(report.chatReportID) : report; @@ -618,6 +623,7 @@ function requestMoney(report, amount, currency, created, merchant, payeeEmail, p receipt, undefined, category, + billable, ); API.write( @@ -638,6 +644,7 @@ function requestMoney(report, amount, currency, created, merchant, payeeEmail, p reportPreviewReportActionID: reportPreviewAction.reportActionID, receipt, category, + billable, }, onyxData, ); @@ -1948,6 +1955,13 @@ function resetMoneyRequestCategory() { Onyx.merge(ONYXKEYS.IOU, {category: ''}); } +/** + * @param {Boolean} billable + */ +function setMoneyRequestBillable(billable) { + Onyx.merge(ONYXKEYS.IOU, {billable}); +} + /** * @param {Object[]} participants */ @@ -2032,6 +2046,7 @@ export { setMoneyRequestMerchant, setMoneyRequestCategory, resetMoneyRequestCategory, + setMoneyRequestBillable, setMoneyRequestParticipants, setMoneyRequestReceipt, createEmptyTransaction, diff --git a/src/libs/actions/Task.js b/src/libs/actions/Task.js index 2d599053ea51..a5e8ac325217 100644 --- a/src/libs/actions/Task.js +++ b/src/libs/actions/Task.js @@ -755,26 +755,50 @@ function getShareDestination(reportID, reports, personalDetails) { * @param {number} originalStatusNum */ function cancelTask(taskReportID, taskTitle, originalStateNum, originalStatusNum) { - const message = `canceled task: ${taskTitle}`; + const message = `deleted task: ${taskTitle}`; const optimisticCancelReportAction = ReportUtils.buildOptimisticTaskReportAction(taskReportID, CONST.REPORT.ACTIONS.TYPE.TASKCANCELLED, message); const optimisticReportActionID = optimisticCancelReportAction.reportActionID; + const taskReport = ReportUtils.getReport(taskReportID); + const parentReportAction = ReportActionsUtils.getParentReportAction(taskReport); + const parentReport = ReportUtils.getParentReport(taskReport); + + const optimisticReportActions = { + [parentReportAction.reportActionID]: { + pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE, + previousMessage: parentReportAction.message, + message: [ + { + translationKey: '', + type: 'COMMENT', + html: '', + text: '', + isEdited: true, + isDeletedParentAction: true, + }, + ], + errors: null, + linkMetaData: [], + }, + }; const optimisticData = [ { onyxMethod: Onyx.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.REPORT}${taskReportID}`, value: { - stateNum: CONST.REPORT.STATE_NUM.SUBMITTED, - statusNum: CONST.REPORT.STATUS.CLOSED, + lastVisibleActionCreated: optimisticCancelReportAction.created, + lastMessageText: message, + lastActorAccountID: optimisticCancelReportAction.actorAccountID, + updateReportInLHN: true, + isDeletedParentAction: true, }, }, { onyxMethod: Onyx.METHOD.MERGE, - key: `${ONYXKEYS.COLLECTION.REPORT}${taskReportID}`, + key: `${ONYXKEYS.COLLECTION.REPORT}${parentReport.reportID}`, value: { - lastVisibleActionCreated: optimisticCancelReportAction.created, - lastMessageText: message, - lastActorAccountID: optimisticCancelReportAction.actorAccountID, + lastMessageText: ReportActionsUtils.getLastVisibleMessage(parentReport.reportID, {[parentReportAction.reportActionID]: null}).lastMessageText, + lastVisibleActionCreated: ReportActionsUtils.getLastVisibleAction(parentReport.reportID, {[parentReportAction.reportActionID]: null}).created, }, }, { @@ -784,6 +808,11 @@ function cancelTask(taskReportID, taskTitle, originalStateNum, originalStatusNum [optimisticReportActionID]: optimisticCancelReportAction, }, }, + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${parentReport.reportID}`, + value: optimisticReportActions, + }, ]; const successData = [ diff --git a/src/pages/ErrorPage/GenericErrorPage.js b/src/pages/ErrorPage/GenericErrorPage.js index 3ff3bc686419..02ab38a1ef20 100644 --- a/src/pages/ErrorPage/GenericErrorPage.js +++ b/src/pages/ErrorPage/GenericErrorPage.js @@ -1,6 +1,6 @@ import React from 'react'; -import PropTypes from 'prop-types'; import {View} from 'react-native'; +import {useErrorBoundary} from 'react-error-boundary'; import Icon from '../../components/Icon'; import defaultTheme from '../../styles/themes/default'; import * as Expensicons from '../../components/Icon/Expensicons'; @@ -19,12 +19,11 @@ import * as StyleUtils from '../../styles/StyleUtils'; const propTypes = { ...withLocalizePropTypes, - - /** Callback to call on refresh button click */ - onRefresh: PropTypes.func.isRequired, }; -function GenericErrorPage(props) { +function GenericErrorPage({translate}) { + const {resetBoundary} = useErrorBoundary(); + return ( {({paddingBottom}) => ( @@ -40,12 +39,12 @@ function GenericErrorPage(props) { /> - {props.translate('genericErrorPage.title')} + {translate('genericErrorPage.title')} - {`${props.translate('genericErrorPage.body.helpTextConcierge')} `} + {`${translate('genericErrorPage.body.helpTextConcierge')} `}