From 0d1e9df22c910fc394a8669a2f951c1bc09edf26 Mon Sep 17 00:00:00 2001 From: Marc Glasser Date: Fri, 24 Nov 2023 11:36:09 -1000 Subject: [PATCH 01/37] Upgrade required idea --- src/CONST.ts | 2 + src/Expensify.js | 13 +++ src/ONYXKEYS.ts | 4 + .../ErrorBoundary/BaseErrorBoundary.tsx | 10 ++- src/components/MenuItem.js | 20 +++-- src/libs/HttpUtils.ts | 5 ++ src/libs/actions/AppUpdate.ts | 6 +- src/pages/ErrorPage/GenericErrorPage.js | 79 +++++++++++-------- src/pages/settings/AppDownloadLinks.js | 68 ++-------------- src/styles/styles.ts | 2 +- 10 files changed, 98 insertions(+), 111 deletions(-) diff --git a/src/CONST.ts b/src/CONST.ts index f1364ebbb5bf..223de7283530 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -731,6 +731,7 @@ const CONST = { EXP_ERROR: 666, MANY_WRITES_ERROR: 665, UNABLE_TO_RETRY: 'unableToRetry', + UPGRADE_REQUIRED: 426, }, HTTP_STATUS: { // When Cloudflare throttles @@ -761,6 +762,7 @@ const CONST = { GATEWAY_TIMEOUT: 'Gateway Timeout', EXPENSIFY_SERVICE_INTERRUPTED: 'Expensify service interrupted', DUPLICATE_RECORD: 'A record already exists with this ID', + UPGRADE_REQUIRED: 'Upgrade Required', }, ERROR_TYPE: { SOCKET: 'Expensify\\Auth\\Error\\Socket', diff --git a/src/Expensify.js b/src/Expensify.js index 1b692f86a197..8050ed99665a 100644 --- a/src/Expensify.js +++ b/src/Expensify.js @@ -36,6 +36,7 @@ import Visibility from './libs/Visibility'; import ONYXKEYS from './ONYXKEYS'; import PopoverReportActionContextMenu from './pages/home/report/ContextMenu/PopoverReportActionContextMenu'; import * as ReportActionContextMenu from './pages/home/report/ContextMenu/ReportActionContextMenu'; +import CONST from './CONST'; Onyx.registerLogger(({level, message}) => { if (level === 'alert') { @@ -76,6 +77,9 @@ const propTypes = { /** Whether the app is waiting for the server's response to determine if a room is public */ isCheckingPublicRoom: PropTypes.bool, + /** True when the user must update to the latest minimum version of the app */ + upgradeRequired: PropTypes.bool, + ...withLocalizePropTypes, }; @@ -88,6 +92,7 @@ const defaultProps = { isSidebarLoaded: false, screenShareRequest: null, isCheckingPublicRoom: true, + upgradeRequired: false, }; const SplashScreenHiddenContext = React.createContext({}); @@ -201,6 +206,10 @@ function Expensify(props) { return null; } + if (props.upgradeRequired) { + throw new Error(CONST.ERROR.UPGRADE_REQUIRED); + } + return ( {shouldInit && ( @@ -261,6 +270,10 @@ export default compose( screenShareRequest: { key: ONYXKEYS.SCREEN_SHARE_REQUEST, }, + upgradeRequired: { + key: ONYXKEYS.UPGRADE_REQUIRED, + initWithStoredValues: false, + } }), )(Expensify); diff --git a/src/ONYXKEYS.ts b/src/ONYXKEYS.ts index 75c284fb9546..b30a2808a24b 100755 --- a/src/ONYXKEYS.ts +++ b/src/ONYXKEYS.ts @@ -234,6 +234,9 @@ const ONYXKEYS = { // Max width supported for HTML element MAX_CANVAS_WIDTH: 'maxCanvasWidth', + /** Indicates whether an forced upgrade is required */ + UPGRADE_REQUIRED: 'upgradeRequired', + /** Collection Keys */ COLLECTION: { DOWNLOAD: 'download_', @@ -417,6 +420,7 @@ type OnyxValues = { [ONYXKEYS.MAX_CANVAS_AREA]: number; [ONYXKEYS.MAX_CANVAS_HEIGHT]: number; [ONYXKEYS.MAX_CANVAS_WIDTH]: number; + [ONYXKEYS.UPGRADE_REQUIRED]: boolean; // Collections [ONYXKEYS.COLLECTION.DOWNLOAD]: OnyxTypes.Download; diff --git a/src/components/ErrorBoundary/BaseErrorBoundary.tsx b/src/components/ErrorBoundary/BaseErrorBoundary.tsx index 2a6524d5a993..2ba78c5f8863 100644 --- a/src/components/ErrorBoundary/BaseErrorBoundary.tsx +++ b/src/components/ErrorBoundary/BaseErrorBoundary.tsx @@ -1,4 +1,4 @@ -import React from 'react'; +import React, { useState } from 'react'; import {ErrorBoundary} from 'react-error-boundary'; import BootSplash from '@libs/BootSplash'; import GenericErrorPage from '@pages/ErrorPage/GenericErrorPage'; @@ -11,15 +11,17 @@ import {BaseErrorBoundaryProps, LogError} from './types'; */ function BaseErrorBoundary({logError = () => {}, errorMessage, children}: BaseErrorBoundaryProps) { - const catchError = (error: Error, errorInfo: React.ErrorInfo) => { - logError(errorMessage, error, JSON.stringify(errorInfo)); + const [error, setError] = useState(() => new Error()); + const catchError = (errorObject: Error, errorInfo: React.ErrorInfo) => { + logError(errorMessage, errorObject, JSON.stringify(errorInfo)); // We hide the splash screen since the error might happened during app init BootSplash.hide(); + setError(errorObject); }; return ( } + fallback={} onError={catchError} > {children} diff --git a/src/components/MenuItem.js b/src/components/MenuItem.js index 9883672976e8..56461f52cc2a 100644 --- a/src/components/MenuItem.js +++ b/src/components/MenuItem.js @@ -175,14 +175,18 @@ const MenuItem = React.forwardRef((props, ref) => { onPressIn={() => props.shouldBlockSelection && isSmallScreenWidth && DeviceCapabilities.canUseTouchScreen() && ControlSelection.block()} onPressOut={ControlSelection.unblock} onSecondaryInteraction={props.onSecondaryInteraction} - style={({pressed}) => [ - props.style, - !props.interactive && styles.cursorDefault, - StyleUtils.getButtonBackgroundColorStyle(getButtonState(props.focused || isHovered, pressed, props.success, props.disabled, props.interactive), true), - (isHovered || pressed) && props.hoverAndPressStyle, - ...(_.isArray(props.wrapperStyle) ? props.wrapperStyle : [props.wrapperStyle]), - props.shouldGreyOutWhenDisabled && props.disabled && styles.buttonOpacityDisabled, - ]} + style={({pressed}) => { + const s = [ + props.style, + !props.interactive && styles.cursorDefault, + StyleUtils.getButtonBackgroundColorStyle(getButtonState(props.focused || isHovered, pressed, props.success, props.disabled, props.interactive), true), + (isHovered || pressed) && props.hoverAndPressStyle, + ...(_.isArray(props.wrapperStyle) ? props.wrapperStyle : [props.wrapperStyle]), + props.shouldGreyOutWhenDisabled && props.disabled && styles.buttonOpacityDisabled, + ]; + console.log({s, style: props.style, ws: props.wrapperStyle}); + return s; + }} disabled={props.disabled} ref={ref} role={CONST.ACCESSIBILITY_ROLE.MENUITEM} diff --git a/src/libs/HttpUtils.ts b/src/libs/HttpUtils.ts index 859c8624833c..d51fb38e0cee 100644 --- a/src/libs/HttpUtils.ts +++ b/src/libs/HttpUtils.ts @@ -7,6 +7,7 @@ import {RequestType} from '@src/types/onyx/Request'; import type Response from '@src/types/onyx/Response'; import * as ApiUtils from './ApiUtils'; import HttpsError from './Errors/HttpsError'; +import * as AppUpdate from './actions/AppUpdate'; let shouldFailAllRequests = false; let shouldForceOffline = false; @@ -103,6 +104,10 @@ function processHTTPRequest(url: string, method: RequestType = 'get', body: Form alert('Too many auth writes', message); } } + if (response.jsonCode === CONST.JSON_CODE.UPGRADE_REQUIRED) { + // Trigger a modal and disable the app as the user needs to upgrade to the latest minimum version to continue + AppUpdate.triggerUpgradeRequired(); + } return response as Promise; }); } diff --git a/src/libs/actions/AppUpdate.ts b/src/libs/actions/AppUpdate.ts index 29ee2a4547ab..8f2ce3ead102 100644 --- a/src/libs/actions/AppUpdate.ts +++ b/src/libs/actions/AppUpdate.ts @@ -9,4 +9,8 @@ function setIsAppInBeta(isBeta: boolean) { Onyx.set(ONYXKEYS.IS_BETA, isBeta); } -export {triggerUpdateAvailable, setIsAppInBeta}; +function triggerUpgradeRequired() { + Onyx.set(ONYXKEYS.UPGRADE_REQUIRED, true); +} + +export {triggerUpdateAvailable, setIsAppInBeta, triggerUpgradeRequired}; diff --git a/src/pages/ErrorPage/GenericErrorPage.js b/src/pages/ErrorPage/GenericErrorPage.js index 7b627a8e18d5..cdf2ff1cea63 100644 --- a/src/pages/ErrorPage/GenericErrorPage.js +++ b/src/pages/ErrorPage/GenericErrorPage.js @@ -1,4 +1,5 @@ import React from 'react'; +import PropTypes from 'prop-types'; import {useErrorBoundary} from 'react-error-boundary'; import {View} from 'react-native'; import LogoWordmark from '@assets/images/expensify-wordmark.svg'; @@ -15,17 +16,21 @@ import useThemeStyles from '@styles/useThemeStyles'; import variables from '@styles/variables'; import * as Session from '@userActions/Session'; import CONST from '@src/CONST'; +import AppDownloadLinksView from '@pages/settings/AppDownloadLinksView'; import ErrorBodyText from './ErrorBodyText'; const propTypes = { + /** Error object handled by the boundary */ + error: PropTypes.instanceOf(Error).isRequired, + ...withLocalizePropTypes, }; -function GenericErrorPage({translate}) { +function GenericErrorPage({translate, error}) { const theme = useTheme(); const styles = useThemeStyles(); const {resetBoundary} = useErrorBoundary(); - + const upgradeRequired = error.message === CONST.ERROR.UPGRADE_REQUIRED; return ( {({paddingBottom}) => ( @@ -34,46 +39,50 @@ function GenericErrorPage({translate}) { - {translate('genericErrorPage.title')} - - - - - {`${translate('genericErrorPage.body.helpTextConcierge')} `} - - {CONST.EMAIL.CONCIERGE} - - - - - -