Skip to content

Commit ec47f1d

Browse files
authored
Better handling of OAuth/Lazy Load App breaking errors (#946)
* Making the full page error dialog more reusable - Converting the reload dialog to it - Adding an OAuth dialog Adding log rocket event for window.opener not being there * New files for the error dialog * Moving the reload dialog * Adding before unload alert * Instead of having a user reload manually just kick off the reload automatically * Adding a quicker handler to prevent infinite loops * Adding comment * Cleaning up code
1 parent 840b4a5 commit ec47f1d

File tree

8 files changed

+158
-73
lines changed

8 files changed

+158
-73
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import { Button, Stack, Typography } from '@mui/material';
2+
import { FormattedMessage } from 'react-intl';
3+
import ErrorDialog from './index';
4+
5+
const ARIA_NAME = 'OauthWindowOpenerMissing';
6+
7+
function OauthWindowOpenerMissing() {
8+
return (
9+
<ErrorDialog
10+
body={
11+
<Stack spacing={2}>
12+
<Typography>
13+
<FormattedMessage id="oauth.windowOpener.error.message1" />
14+
</Typography>
15+
<Typography>
16+
<FormattedMessage id="oauth.windowOpener.error.message2" />
17+
</Typography>
18+
</Stack>
19+
}
20+
bodyTitle={<FormattedMessage id="oauth.windowOpener.error.title" />}
21+
cta={
22+
<Button
23+
onClick={() => {
24+
window.close();
25+
}}
26+
>
27+
<FormattedMessage id="cta.close" />
28+
</Button>
29+
}
30+
dialogTitle={
31+
<FormattedMessage id="oauth.windowOpener.error.dialog.title" />
32+
}
33+
name={ARIA_NAME}
34+
/>
35+
);
36+
}
37+
38+
export default OauthWindowOpenerMissing;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import {
2+
Dialog,
3+
DialogActions,
4+
DialogContent,
5+
DialogContentText,
6+
DialogTitle,
7+
} from '@mui/material';
8+
import { ReactNode } from 'react';
9+
import AlertBox from '../AlertBox';
10+
11+
const ARIA_DESC_ID = '-dialog-description';
12+
const ARIA_LABEL_ID = '-dialog-title';
13+
14+
interface Props {
15+
body: ReactNode;
16+
cta: ReactNode;
17+
name: string;
18+
dialogTitle: ReactNode;
19+
bodyTitle: ReactNode;
20+
}
21+
22+
function ErrorDialog({ name, dialogTitle, bodyTitle, body, cta }: Props) {
23+
const labelledBy = `${name}${ARIA_LABEL_ID}`;
24+
const describedby = `${name}${ARIA_DESC_ID}`;
25+
26+
return (
27+
<Dialog
28+
open
29+
aria-labelledby={labelledBy}
30+
aria-describedby={describedby}
31+
sx={{
32+
minWidth: 300,
33+
}}
34+
>
35+
<DialogTitle id={labelledBy}>{dialogTitle}</DialogTitle>
36+
<DialogContent>
37+
<AlertBox short severity="info" title={bodyTitle}>
38+
<DialogContentText id={describedby} component="div">
39+
{body}
40+
</DialogContentText>
41+
</AlertBox>
42+
</DialogContent>
43+
<DialogActions>{cta}</DialogActions>
44+
</Dialog>
45+
);
46+
}
47+
48+
export default ErrorDialog;

src/components/shared/MustReloadDialog.tsx

-63
This file was deleted.

src/forms/renderers/OAuth/useOauthHandler.ts

+9-1
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
import { accessToken, authURL } from 'api/oauth';
22
import { useOAuth2 } from 'hooks/forks/react-use-oauth2/components';
33
import { isEmpty } from 'lodash';
4-
import { useState } from 'react';
4+
import { useCallback, useState } from 'react';
55
import { useIntl } from 'react-intl';
6+
import { useBeforeUnload } from 'react-use';
67
import { useDetailsForm_connectorImage_connectorId } from 'stores/DetailsForm/hooks';
78
import { useEndpointConfigStore_endpointConfig_data } from 'stores/EndpointConfig/hooks';
89
import { CREDENTIALS, INJECTED_VALUES } from './shared';
@@ -94,6 +95,13 @@ export const useOauthHandler = (
9495
}
9596
};
9697

98+
// Loading usually means there is an OAuth in progress so make sure
99+
// they want to close the window.
100+
const when = useCallback(() => {
101+
return loading;
102+
}, [loading]);
103+
useBeforeUnload(when, intl.formatMessage({ id: 'confirm.oauth' }));
104+
97105
return {
98106
errorMessage,
99107
openPopUp,

src/hooks/forks/react-use-oauth2/components/OAuthPopup.tsx

+16-2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
// Heavily edited version of https://github.com/tasoskakour/react-use-oauth2
2+
23
import FullPageSpinner from 'components/fullPage/Spinner';
3-
import { useEffect } from 'react';
4+
import OauthWindowOpenerMissing from 'components/shared/ErrorDialog/OauthWindowOpenerMissing';
5+
import { useEffect, useState } from 'react';
6+
import { logRocketEvent } from 'services/shared';
7+
import { CustomEvents } from 'services/types';
48
import { base64RemovePadding } from 'utils/misc-utils';
59
import { OAUTH_RESPONSE, OAUTH_STATE_KEY } from './constants';
610

@@ -26,6 +30,7 @@ const sendMessage = (body: any) => {
2630
};
2731

2832
const OAuthPopup = (props: Props) => {
33+
const [showError, setShowError] = useState(false);
2934
const { Component = <FullPageSpinner delay={0} /> } = props;
3035

3136
// On mount
@@ -37,7 +42,12 @@ const OAuthPopup = (props: Props) => {
3742
const { error, state } = payload;
3843

3944
if (!window.opener) {
40-
throw new Error('No window opener');
45+
logRocketEvent(CustomEvents.OAUTH_WINDOW_OPENER, {
46+
missing: true,
47+
});
48+
49+
setShowError(true);
50+
return;
4151
}
4252

4353
if (error) {
@@ -55,6 +65,10 @@ const OAuthPopup = (props: Props) => {
5565
}
5666
}, []);
5767

68+
if (showError) {
69+
return <OauthWindowOpenerMissing />;
70+
}
71+
5872
return Component;
5973
};
6074

src/lang/en-US.ts

+5-5
Original file line numberDiff line numberDiff line change
@@ -195,16 +195,12 @@ const ErrorBoundry: ResolvedIntlConfig['messages'] = {
195195
'errorBoundry.title': `${Error['error.title']}`,
196196
'errorBoundry.message1': `There was an unexpected application error. `,
197197
'errorBoundry.message2': `Expand to see details.`,
198-
'errorBoundry.chunkNotFetched.dialog.title': `Reload Required`,
199-
'errorBoundry.chunkNotFetched.error.title': `Failure to load`,
200-
'errorBoundry.chunkNotFetched.error.message1': `There was an issue fetching this portion of the application.`,
201-
'errorBoundry.chunkNotFetched.error.message2': `This is usually caused by a network issue or an old dashboard version being cached.`,
202-
'errorBoundry.chunkNotFetched.error.instructions': `To continue please reload.`,
203198
};
204199

205200
const ConfirmationDialog: ResolvedIntlConfig['messages'] = {
206201
'confirm.title': `Are you sure?`,
207202
'confirm.loseData': `You have unsaved work. If you continue, you will lose your changes.`,
203+
'confirm.oauth': `You are in the process of OAuth. If you continue, you will lose your changes.`,
208204
};
209205

210206
const FullPage: ResolvedIntlConfig['messages'] = {
@@ -1039,6 +1035,10 @@ const OAuth: ResolvedIntlConfig['messages'] = {
10391035
'oauth.authenticate': `Authenticate your {provider} account`,
10401036
'oauth.remove': `Remove`,
10411037
'oauth.edit.message': `If you edit your endpoint config and want to continue using OAuth you must reauthenticate.`,
1038+
'oauth.windowOpener.error.dialog.title': `OAuth Failed`,
1039+
'oauth.windowOpener.error.title': `Cannot reach parent window`,
1040+
'oauth.windowOpener.error.message1': `We are unable to communicate with the window that opened the OAuth pop up. The window may have been closed.`,
1041+
'oauth.windowOpener.error.message2': `Please close this dialog and try again.`,
10421042
};
10431043

10441044
const Supabase: ResolvedIntlConfig['messages'] = {

src/services/react.ts

+41-2
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,54 @@
1-
import MustReloadDialog from 'components/shared/MustReloadDialog';
1+
import FullPageSpinner from 'components/fullPage/Spinner';
22
import { lazy } from 'react';
33
import { logRocketConsole, logRocketEvent } from './shared';
44
import { CustomEvents } from './types';
55

6+
const LAZY_LOAD_FAILED_KEY = 'chunk_failed';
7+
8+
// https://mitchgavan.com/code-splitting-react-safely/
9+
const setWithExpiry = (key: string, value: string, ttl: number) => {
10+
localStorage.setItem(
11+
key,
12+
JSON.stringify({
13+
value,
14+
expiry: new Date().getTime() + ttl,
15+
})
16+
);
17+
};
18+
19+
const getWithExpiry = (key: string) => {
20+
const itemString = window.localStorage.getItem(key);
21+
const item = itemString ? JSON.parse(itemString) : null;
22+
23+
// Either there is no setting or we couldn't parse it. Either way we should try reloading again
24+
if (item === null) {
25+
return null;
26+
}
27+
28+
// We have waited long enough to allow trying again so clearing out the key
29+
if (new Date().getTime() > item.expiry) {
30+
localStorage.removeItem(key);
31+
return null;
32+
}
33+
34+
// Return value so we do not try reloading again
35+
return item.value;
36+
};
37+
638
const handledLazy = (factory: () => Promise<{ default: any }>) => {
739
return lazy(() =>
840
factory().catch((error) => {
941
logRocketEvent(CustomEvents.LAZY_LOADING, 'failed');
1042
logRocketConsole('Component Failed Loading:', error);
1143

12-
return { default: MustReloadDialog };
44+
// Check if we're in an infinite loading loop before trying again
45+
if (!getWithExpiry(LAZY_LOAD_FAILED_KEY)) {
46+
setWithExpiry(LAZY_LOAD_FAILED_KEY, 'true', 10000);
47+
window.location.reload();
48+
}
49+
50+
// Still make sure to return something so the app does not blow up
51+
return { default: FullPageSpinner };
1352
})
1453
);
1554
};

src/services/types.ts

+1
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ export enum CustomEvents {
3030
MATERIALIZATION_TEST = 'Materialization_Test',
3131
MATERIALIZATION_TEST_BACKGROUND = 'Materialization_Test_Background',
3232
NOTIFICATION_NETWORK_WARNING = 'Notification_Network_Warning',
33+
OAUTH_WINDOW_OPENER = 'Oauth_Window_Opener',
3334
STRIPE_FORM_LOADING_FAILED = 'Stripe_Form_Loading_Failed',
3435
SUPABASE_CALL_FAILED = 'Supabase_Call_Failed',
3536
SUPABASE_CALL_UNAUTHENTICATED = 'Supabase_Call_Unauthenticated',

0 commit comments

Comments
 (0)