diff --git a/assets/animations/Update.lottie b/assets/animations/Update.lottie
new file mode 100644
index 000000000000..363486ec2267
Binary files /dev/null and b/assets/animations/Update.lottie differ
diff --git a/src/CONST.ts b/src/CONST.ts
index cea26c789799..5fee60e57617 100755
--- a/src/CONST.ts
+++ b/src/CONST.ts
@@ -788,6 +788,7 @@ const CONST = {
EXP_ERROR: 666,
MANY_WRITES_ERROR: 665,
UNABLE_TO_RETRY: 'unableToRetry',
+ UPDATE_REQUIRED: 426,
},
HTTP_STATUS: {
// When Cloudflare throttles
@@ -818,6 +819,9 @@ const CONST = {
GATEWAY_TIMEOUT: 'Gateway Timeout',
EXPENSIFY_SERVICE_INTERRUPTED: 'Expensify service interrupted',
DUPLICATE_RECORD: 'A record already exists with this ID',
+
+ // The "Upgrade" is intentional as the 426 HTTP code means "Upgrade Required" and sent by the API. We use the "Update" language everywhere else in the front end when this gets returned.
+ UPDATE_REQUIRED: 'Upgrade Required',
},
ERROR_TYPE: {
SOCKET: 'Expensify\\Auth\\Error\\Socket',
diff --git a/src/Expensify.js b/src/Expensify.js
index 0707ba069241..12003968b284 100644
--- a/src/Expensify.js
+++ b/src/Expensify.js
@@ -13,6 +13,7 @@ import AppleAuthWrapper from './components/SignInButtons/AppleAuthWrapper';
import SplashScreenHider from './components/SplashScreenHider';
import UpdateAppModal from './components/UpdateAppModal';
import withLocalize, {withLocalizePropTypes} from './components/withLocalize';
+import CONST from './CONST';
import * as EmojiPickerAction from './libs/actions/EmojiPickerAction';
import * as Report from './libs/actions/Report';
import * as User from './libs/actions/User';
@@ -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 */
+ updateRequired: PropTypes.bool,
+
/** Whether we should display the notification alerting the user that focus mode has been auto-enabled */
focusModeNotification: PropTypes.bool,
@@ -91,6 +95,7 @@ const defaultProps = {
isSidebarLoaded: false,
screenShareRequest: null,
isCheckingPublicRoom: true,
+ updateRequired: false,
focusModeNotification: false,
};
@@ -204,6 +209,10 @@ function Expensify(props) {
return null;
}
+ if (props.updateRequired) {
+ throw new Error(CONST.ERROR.UPDATE_REQUIRED);
+ }
+
return (
{/* We include the modal for showing a new update at the top level so the option is always present. */}
- {props.updateAvailable ? : null}
+ {/* If the update is required we won't show this option since a full screen update view will be displayed instead. */}
+ {props.updateAvailable && !props.updateRequired ? : null}
{props.screenShareRequest ? (
element
MAX_CANVAS_WIDTH: 'maxCanvasWidth',
+ /** Indicates whether an forced upgrade is required */
+ UPDATE_REQUIRED: 'updateRequired',
+
/** Collection Keys */
COLLECTION: {
DOWNLOAD: 'download_',
@@ -442,6 +445,7 @@ type OnyxValues = {
[ONYXKEYS.MAX_CANVAS_AREA]: number;
[ONYXKEYS.MAX_CANVAS_HEIGHT]: number;
[ONYXKEYS.MAX_CANVAS_WIDTH]: number;
+ [ONYXKEYS.UPDATE_REQUIRED]: boolean;
// Collections
[ONYXKEYS.COLLECTION.DOWNLOAD]: OnyxTypes.Download;
diff --git a/src/components/ErrorBoundary/BaseErrorBoundary.tsx b/src/components/ErrorBoundary/BaseErrorBoundary.tsx
index 6a0f1a0ae55e..2f775aa4bef1 100644
--- a/src/components/ErrorBoundary/BaseErrorBoundary.tsx
+++ b/src/components/ErrorBoundary/BaseErrorBoundary.tsx
@@ -1,7 +1,9 @@
-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';
+import UpdateRequiredView from '@pages/ErrorPage/UpdateRequiredView';
+import CONST from '@src/CONST';
import type {BaseErrorBoundaryProps, LogError} from './types';
/**
@@ -11,15 +13,19 @@ import type {BaseErrorBoundaryProps, LogError} from './types';
*/
function BaseErrorBoundary({logError = () => {}, errorMessage, children}: BaseErrorBoundaryProps) {
- const catchError = (error: Error, errorInfo: React.ErrorInfo) => {
- logError(errorMessage, error, JSON.stringify(errorInfo));
+ const [errorContent, setErrorContent] = useState('');
+ 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();
+ setErrorContent(errorObject.message);
};
+ const updateRequired = errorContent === CONST.ERROR.UPDATE_REQUIRED;
+
return (
}
+ fallback={updateRequired ? : }
onError={catchError}
>
{children}
diff --git a/src/components/LottieAnimations/index.tsx b/src/components/LottieAnimations/index.tsx
index d42d471eba5e..fd593421232d 100644
--- a/src/components/LottieAnimations/index.tsx
+++ b/src/components/LottieAnimations/index.tsx
@@ -1,3 +1,4 @@
+import variables from '@styles/variables';
import type DotLottieAnimation from './types';
const DotLottieAnimations: Record = {
@@ -51,6 +52,11 @@ const DotLottieAnimations: Record = {
w: 853,
h: 480,
},
+ Update: {
+ file: require('@assets/animations/Update.lottie'),
+ w: variables.updateAnimationW,
+ h: variables.updateAnimationH,
+ },
Coin: {
file: require('@assets/animations/Coin.lottie'),
w: 375,
diff --git a/src/languages/en.ts b/src/languages/en.ts
index bb22c7a7856a..712113cb89a9 100755
--- a/src/languages/en.ts
+++ b/src/languages/en.ts
@@ -299,6 +299,7 @@ export default {
showing: 'Showing',
of: 'of',
default: 'Default',
+ update: 'Update',
},
location: {
useCurrent: 'Use current location',
@@ -772,6 +773,11 @@ export default {
isShownOnProfile: 'Your timezone is shown on your profile.',
getLocationAutomatically: 'Automatically determine your location.',
},
+ updateRequiredView: {
+ updateRequired: 'Update required',
+ pleaseInstall: 'Please update to the latest version of New Expensify',
+ toGetLatestChanges: 'For mobile or desktop, download and install the latest version. For web, refresh your browser.',
+ },
initialSettingsPage: {
about: 'About',
aboutPage: {
diff --git a/src/languages/es.ts b/src/languages/es.ts
index 858fe29a8faf..d46f275a8109 100644
--- a/src/languages/es.ts
+++ b/src/languages/es.ts
@@ -288,6 +288,7 @@ export default {
showing: 'Mostrando',
of: 'de',
default: 'Predeterminado',
+ update: 'Actualizar',
},
location: {
useCurrent: 'Usar ubicación actual',
@@ -766,6 +767,11 @@ export default {
isShownOnProfile: 'Tu zona horaria se muestra en tu perfil.',
getLocationAutomatically: 'Detecta tu ubicación automáticamente.',
},
+ updateRequiredView: {
+ updateRequired: 'Actualización requerida',
+ pleaseInstall: 'Por favor, actualice la última versión de Nuevo Expensify',
+ toGetLatestChanges: 'Para móvil o escritorio, descarga e instala la última versión. Para la web, actualiza tu navegador.',
+ },
initialSettingsPage: {
about: 'Acerca de',
aboutPage: {
diff --git a/src/libs/Environment/betaChecker/index.android.ts b/src/libs/Environment/betaChecker/index.android.ts
index aeb1527457f7..4b912e0daaa5 100644
--- a/src/libs/Environment/betaChecker/index.android.ts
+++ b/src/libs/Environment/betaChecker/index.android.ts
@@ -1,6 +1,6 @@
import Onyx from 'react-native-onyx';
import semver from 'semver';
-import * as AppUpdate from '@userActions/AppUpdate';
+import * as AppUpdate from '@libs/actions/AppUpdate';
import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
import pkg from '../../../../package.json';
diff --git a/src/libs/HttpUtils.ts b/src/libs/HttpUtils.ts
index 22e342ac847b..16afc377bba3 100644
--- a/src/libs/HttpUtils.ts
+++ b/src/libs/HttpUtils.ts
@@ -6,6 +6,7 @@ import ONYXKEYS from '@src/ONYXKEYS';
import type {RequestType} from '@src/types/onyx/Request';
import type Response from '@src/types/onyx/Response';
import * as NetworkActions from './actions/Network';
+import * as UpdateRequired from './actions/UpdateRequired';
import * as ApiUtils from './ApiUtils';
import HttpsError from './Errors/HttpsError';
@@ -128,6 +129,10 @@ function processHTTPRequest(url: string, method: RequestType = 'get', body: Form
alert('Too many auth writes', message);
}
}
+ if (response.jsonCode === CONST.JSON_CODE.UPDATE_REQUIRED) {
+ // Trigger a modal and disable the app as the user needs to upgrade to the latest minimum version to continue
+ UpdateRequired.alertUser();
+ }
return response as Promise;
});
}
diff --git a/src/libs/Notification/LocalNotification/BrowserNotifications.ts b/src/libs/Notification/LocalNotification/BrowserNotifications.ts
index e65bd3d0021f..0c3f3ec60203 100644
--- a/src/libs/Notification/LocalNotification/BrowserNotifications.ts
+++ b/src/libs/Notification/LocalNotification/BrowserNotifications.ts
@@ -2,9 +2,9 @@
import Str from 'expensify-common/lib/str';
import type {ImageSourcePropType} from 'react-native';
import EXPENSIFY_ICON_URL from '@assets/images/expensify-logo-round-clearspace.png';
+import * as AppUpdate from '@libs/actions/AppUpdate';
import ModifiedExpenseMessage from '@libs/ModifiedExpenseMessage';
import * as ReportUtils from '@libs/ReportUtils';
-import * as AppUpdate from '@userActions/AppUpdate';
import type {Report, ReportAction} from '@src/types/onyx';
import focusApp from './focusApp';
import type {LocalNotificationClickHandler, LocalNotificationData} from './types';
diff --git a/src/libs/actions/AppUpdate.ts b/src/libs/actions/AppUpdate/index.ts
similarity index 71%
rename from src/libs/actions/AppUpdate.ts
rename to src/libs/actions/AppUpdate/index.ts
index 29ee2a4547ab..69c80a089831 100644
--- a/src/libs/actions/AppUpdate.ts
+++ b/src/libs/actions/AppUpdate/index.ts
@@ -1,5 +1,6 @@
import Onyx from 'react-native-onyx';
import ONYXKEYS from '@src/ONYXKEYS';
+import updateApp from './updateApp';
function triggerUpdateAvailable() {
Onyx.set(ONYXKEYS.UPDATE_AVAILABLE, true);
@@ -9,4 +10,4 @@ function setIsAppInBeta(isBeta: boolean) {
Onyx.set(ONYXKEYS.IS_BETA, isBeta);
}
-export {triggerUpdateAvailable, setIsAppInBeta};
+export {triggerUpdateAvailable, setIsAppInBeta, updateApp};
diff --git a/src/libs/actions/AppUpdate/updateApp/index.android.ts b/src/libs/actions/AppUpdate/updateApp/index.android.ts
new file mode 100644
index 000000000000..f6a6387a8aef
--- /dev/null
+++ b/src/libs/actions/AppUpdate/updateApp/index.android.ts
@@ -0,0 +1,6 @@
+import * as Link from '@userActions/Link';
+import CONST from '@src/CONST';
+
+export default function updateApp() {
+ Link.openExternalLink(CONST.APP_DOWNLOAD_LINKS.ANDROID);
+}
diff --git a/src/libs/actions/AppUpdate/updateApp/index.desktop.ts b/src/libs/actions/AppUpdate/updateApp/index.desktop.ts
new file mode 100644
index 000000000000..fb3a7d649baa
--- /dev/null
+++ b/src/libs/actions/AppUpdate/updateApp/index.desktop.ts
@@ -0,0 +1,6 @@
+import {Linking} from 'react-native';
+import CONST from '@src/CONST';
+
+export default function updateApp() {
+ Linking.openURL(CONST.APP_DOWNLOAD_LINKS.DESKTOP);
+}
diff --git a/src/libs/actions/AppUpdate/updateApp/index.ios.ts b/src/libs/actions/AppUpdate/updateApp/index.ios.ts
new file mode 100644
index 000000000000..8b66521bb9c8
--- /dev/null
+++ b/src/libs/actions/AppUpdate/updateApp/index.ios.ts
@@ -0,0 +1,6 @@
+import * as Link from '@userActions/Link';
+import CONST from '@src/CONST';
+
+export default function updateApp() {
+ Link.openExternalLink(CONST.APP_DOWNLOAD_LINKS.IOS);
+}
diff --git a/src/libs/actions/AppUpdate/updateApp/index.ts b/src/libs/actions/AppUpdate/updateApp/index.ts
new file mode 100644
index 000000000000..8c2b191029a2
--- /dev/null
+++ b/src/libs/actions/AppUpdate/updateApp/index.ts
@@ -0,0 +1,6 @@
+/**
+ * On web or mWeb we can simply refresh the page and the user should have the new version of the app downloaded.
+ */
+export default function updateApp() {
+ window.location.reload();
+}
diff --git a/src/libs/actions/UpdateRequired.ts b/src/libs/actions/UpdateRequired.ts
new file mode 100644
index 000000000000..26f0a119ac8d
--- /dev/null
+++ b/src/libs/actions/UpdateRequired.ts
@@ -0,0 +1,22 @@
+import Onyx from 'react-native-onyx';
+import getEnvironment from '@libs/Environment/getEnvironment';
+import CONST from '@src/CONST';
+import ONYXKEYS from '@src/ONYXKEYS';
+
+function alertUser() {
+ // For now, we will pretty much never have to do this on a platform other than production.
+ // We should only update the minimum app version in the API after all platforms of a new version have been deployed to PRODUCTION.
+ // As staging is always ahead of production there is no reason to "force update" those apps.
+ getEnvironment().then((environment) => {
+ if (environment !== CONST.ENVIRONMENT.PRODUCTION) {
+ return;
+ }
+
+ Onyx.set(ONYXKEYS.UPDATE_REQUIRED, true);
+ });
+}
+
+export {
+ // eslint-disable-next-line import/prefer-default-export
+ alertUser,
+};
diff --git a/src/libs/migrations/PersonalDetailsByAccountID.js b/src/libs/migrations/PersonalDetailsByAccountID.js
index c08ec6fb2c43..24aece8f5a97 100644
--- a/src/libs/migrations/PersonalDetailsByAccountID.js
+++ b/src/libs/migrations/PersonalDetailsByAccountID.js
@@ -251,12 +251,6 @@ export default function () {
delete newReport.lastActorEmail;
}
- if (lodashHas(newReport, ['participants'])) {
- reportWasModified = true;
- Log.info(`[Migrate Onyx] PersonalDetailsByAccountID migration: removing participants from report ${newReport.reportID}`);
- delete newReport.participants;
- }
-
if (lodashHas(newReport, ['ownerEmail'])) {
reportWasModified = true;
Log.info(`[Migrate Onyx] PersonalDetailsByAccountID migration: removing ownerEmail from report ${newReport.reportID}`);
diff --git a/src/pages/ErrorPage/UpdateRequiredView.tsx b/src/pages/ErrorPage/UpdateRequiredView.tsx
new file mode 100644
index 000000000000..2a73215d2293
--- /dev/null
+++ b/src/pages/ErrorPage/UpdateRequiredView.tsx
@@ -0,0 +1,60 @@
+import React from 'react';
+import {View} from 'react-native';
+import Button from '@components/Button';
+import Header from '@components/Header';
+import HeaderGap from '@components/HeaderGap';
+import Lottie from '@components/Lottie';
+import LottieAnimations from '@components/LottieAnimations';
+import Text from '@components/Text';
+import useLocalize from '@hooks/useLocalize';
+import useSafeAreaInsets from '@hooks/useSafeAreaInsets';
+import useStyleUtils from '@hooks/useStyleUtils';
+import useThemeStyles from '@hooks/useThemeStyles';
+import useWindowDimensions from '@hooks/useWindowDimensions';
+import * as AppUpdate from '@libs/actions/AppUpdate';
+
+function UpdateRequiredView() {
+ const insets = useSafeAreaInsets();
+ const styles = useThemeStyles();
+ const StyleUtils = useStyleUtils();
+ const {translate} = useLocalize();
+ const {isSmallScreenWidth} = useWindowDimensions();
+ return (
+
+
+
+
+
+
+
+
+
+
+ {translate('updateRequiredView.pleaseInstall')}
+
+
+ {translate('updateRequiredView.toGetLatestChanges')}
+
+
+
+
+
+ );
+}
+
+UpdateRequiredView.displayName = 'UpdateRequiredView';
+export default UpdateRequiredView;
diff --git a/src/styles/index.ts b/src/styles/index.ts
index 9bfe407593df..34f69faa89cc 100644
--- a/src/styles/index.ts
+++ b/src/styles/index.ts
@@ -4169,6 +4169,19 @@ const styles = (theme: ThemeColors) =>
},
colorSchemeStyle: (colorScheme: ColorScheme) => ({colorScheme}),
+
+ updateAnimation: {
+ width: variables.updateAnimationW,
+ height: variables.updateAnimationH,
+ },
+
+ updateRequiredViewHeader: {
+ height: variables.updateViewHeaderHeight,
+ },
+
+ updateRequiredViewTextContainer: {
+ width: variables.updateTextViewContainerWidth,
+ },
} satisfies Styles);
type ThemeStyles = ReturnType;
diff --git a/src/styles/utils/index.ts b/src/styles/utils/index.ts
index ea8b3f258c89..08787d18f6dc 100644
--- a/src/styles/utils/index.ts
+++ b/src/styles/utils/index.ts
@@ -1431,6 +1431,14 @@ const createStyleUtils = (theme: ThemeColors, styles: ThemeStyles) => ({
return containerStyles;
},
+ getUpdateRequiredViewStyles: (isSmallScreenWidth: boolean): ViewStyle[] => [
+ {
+ alignItems: 'center',
+ justifyContent: 'center',
+ ...(isSmallScreenWidth ? {} : styles.pb40),
+ },
+ ],
+
getFullscreenCenteredContentStyles: () => [StyleSheet.absoluteFill, styles.justifyContentCenter, styles.alignItemsCenter],
});
diff --git a/src/styles/utils/spacing.ts b/src/styles/utils/spacing.ts
index 6def4858229f..bb0e797e5812 100644
--- a/src/styles/utils/spacing.ts
+++ b/src/styles/utils/spacing.ts
@@ -533,6 +533,10 @@ export default {
paddingBottom: 80,
},
+ pb40: {
+ paddingBottom: 160,
+ },
+
pb10Percentage: {
paddingBottom: '10%',
},
diff --git a/src/styles/variables.ts b/src/styles/variables.ts
index b11d48898af5..d966b7829bc9 100644
--- a/src/styles/variables.ts
+++ b/src/styles/variables.ts
@@ -198,4 +198,8 @@ export default {
cardPreviewWidth: 235,
cardNameWidth: 156,
holdMenuIconSize: 64,
+ updateAnimationW: 390,
+ updateAnimationH: 240,
+ updateTextViewContainerWidth: 310,
+ updateViewHeaderHeight: 70,
} as const;
diff --git a/tests/unit/MigrationTest.js b/tests/unit/MigrationTest.js
index ebffc71e4e0e..28d21cd4b11c 100644
--- a/tests/unit/MigrationTest.js
+++ b/tests/unit/MigrationTest.js
@@ -428,31 +428,6 @@ describe('Migrations', () => {
});
}));
- it('Should remove any instances of participants found in a report', () =>
- Onyx.multiSet({
- [`${ONYXKEYS.COLLECTION.REPORT}1`]: {
- reportID: 1,
- participants: ['fake@test.com'],
- participantAccountIDs: [5],
- },
- })
- .then(PersonalDetailsByAccountID)
- .then(() => {
- expect(LogSpy).toHaveBeenCalledWith('[Migrate Onyx] PersonalDetailsByAccountID migration: removing participants from report 1');
- const connectionID = Onyx.connect({
- key: ONYXKEYS.COLLECTION.REPORT,
- waitForCollectionCallback: true,
- callback: (allReports) => {
- Onyx.disconnect(connectionID);
- const expectedReport = {
- reportID: 1,
- participantAccountIDs: [5],
- };
- expect(allReports[`${ONYXKEYS.COLLECTION.REPORT}1`]).toMatchObject(expectedReport);
- },
- });
- }));
-
it('Should remove any instances of ownerEmail found in a report', () =>
Onyx.multiSet({
[`${ONYXKEYS.COLLECTION.REPORT}1`]: {