Skip to content

Commit ca419d5

Browse files
authored
refactor(account-center): get access token hook (#8020)
refactor(account-center): session error boundry
1 parent 39293e3 commit ca419d5

File tree

12 files changed

+113
-129
lines changed

12 files changed

+113
-129
lines changed

packages/account-center/src/App.tsx

Lines changed: 9 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -4,25 +4,20 @@ import { useContext, useEffect } from 'react';
44
import { BrowserRouter, Route, Routes } from 'react-router-dom';
55

66
import AppBoundary from '@ac/Providers/AppBoundary';
7+
import ErrorBoundary from '@ac/Providers/AppBoundary/ErrorBoundary';
8+
import LogtoErrorBoundary from '@ac/Providers/AppBoundary/LogtoErrorBoundary';
79
import LoadingContextProvider from '@ac/Providers/LoadingContextProvider';
810

911
import styles from './App.module.scss';
1012
import Callback from './Callback';
1113
import PageContextProvider from './Providers/PageContextProvider';
1214
import PageContext from './Providers/PageContextProvider/PageContext';
1315
import BrandingHeader from './components/BrandingHeader';
14-
import ErrorPage from './components/ErrorPage';
15-
import {
16-
emailRoute,
17-
phoneRoute,
18-
sessionExpiredRoute,
19-
updateSuccessRoute,
20-
} from './constants/routes';
16+
import { emailRoute, phoneRoute, updateSuccessRoute } from './constants/routes';
2117
import initI18n from './i18n/init';
2218
import Email from './pages/Email';
2319
import Home from './pages/Home';
2420
import Phone from './pages/Phone';
25-
import SessionExpired from './pages/SessionExpired';
2621
import UpdateSuccess from './pages/UpdateSuccess';
2722
import { accountCenterBasePath, handleAccountCenterRoute } from './utils/account-center-route';
2823

@@ -37,7 +32,7 @@ const Main = () => {
3732
const params = new URLSearchParams(window.location.search);
3833
const isInCallback = Boolean(params.get('code'));
3934
const { isAuthenticated, isLoading, signIn } = useLogto();
40-
const { isLoadingExperience, experienceError, userInfoError } = useContext(PageContext);
35+
const { isLoadingExperience } = useContext(PageContext);
4136
const isInitialAuthLoading = !isAuthenticated && isLoading;
4237

4338
useEffect(() => {
@@ -54,15 +49,6 @@ const Main = () => {
5449
return <Callback />;
5550
}
5651

57-
if (experienceError ?? userInfoError) {
58-
return (
59-
<ErrorPage
60-
titleKey="error.something_went_wrong"
61-
rawMessage="We were unable to load your experience settings. Please refresh the page."
62-
/>
63-
);
64-
}
65-
6652
if (isInitialAuthLoading || isLoadingExperience) {
6753
return <div className={styles.status}>Loading…</div>;
6854
}
@@ -73,7 +59,6 @@ const Main = () => {
7359

7460
return (
7561
<Routes>
76-
<Route path={sessionExpiredRoute} element={<SessionExpired />} />
7762
<Route path={updateSuccessRoute} element={<UpdateSuccess />} />
7863
<Route path={emailRoute} element={<Email />} />
7964
<Route path={phoneRoute} element={<Phone />} />
@@ -100,7 +85,11 @@ const App = () => (
10085
<div className={styles.layout}>
10186
<div className={styles.container}>
10287
<main className={styles.main}>
103-
<Main />
88+
<ErrorBoundary>
89+
<LogtoErrorBoundary>
90+
<Main />
91+
</LogtoErrorBoundary>
92+
</ErrorBoundary>
10493
</main>
10594
</div>
10695
</div>
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
import { LogtoClientError, LogtoError, LogtoRequestError } from '@logto/react';
2+
import { HTTPError } from 'ky';
3+
import type { ReactNode } from 'react';
4+
import { Component } from 'react';
5+
6+
import ErrorPage from '@ac/components/ErrorPage';
7+
import SessionExpired from '@ac/pages/SessionExpired';
8+
9+
const isOidcInvalidGrantError = (error: Error) => {
10+
if (!(error instanceof LogtoRequestError)) {
11+
return false;
12+
}
13+
14+
const oidcGrantErrors = ['oidc.invalid_grant', 'oidc.invalid_target'];
15+
16+
return oidcGrantErrors.includes(error.code);
17+
};
18+
19+
type Props = {
20+
readonly children: ReactNode;
21+
};
22+
23+
type State = {
24+
error?: Error;
25+
};
26+
27+
class ErrorBoundary extends Component<Props, State> {
28+
static getDerivedStateFromError(error: Error): State {
29+
return { error };
30+
}
31+
32+
public state: State = {};
33+
34+
render() {
35+
const { children } = this.props;
36+
const { error } = this.state;
37+
38+
if (!error) {
39+
return children;
40+
}
41+
42+
if (
43+
error instanceof LogtoError ||
44+
error instanceof LogtoClientError ||
45+
isOidcInvalidGrantError(error) ||
46+
(error instanceof HTTPError && error.response.status === 401)
47+
) {
48+
return <SessionExpired />;
49+
}
50+
51+
return <ErrorPage titleKey="error.something_went_wrong" rawMessage={error.message} />;
52+
}
53+
}
54+
55+
export default ErrorBoundary;
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import { useLogto } from '@logto/react';
2+
import type { ReactElement } from 'react';
3+
import { useEffect } from 'react';
4+
5+
/**
6+
* Keep children untouched but throw Logto errors so upper error boundary can handle them.
7+
*/
8+
const LogtoErrorBoundary = ({ children }: { readonly children: ReactElement }) => {
9+
const { error } = useLogto();
10+
11+
useEffect(() => {
12+
if (error) {
13+
throw error;
14+
}
15+
}, [error]);
16+
17+
return children;
18+
};
19+
20+
export default LogtoErrorBoundary;

packages/account-center/src/Providers/PageContextProvider/index.tsx

Lines changed: 11 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,11 @@
11
import { useLogto } from '@logto/react';
22
import { Theme } from '@logto/schemas';
3-
import { HTTPError } from 'ky';
43
import { useCallback, useEffect, useMemo, useState } from 'react';
5-
import { useNavigate } from 'react-router-dom';
64

75
import { getAccountCenterSettings } from '@ac/apis/account-center';
86
import { getSignInExperienceSettings } from '@ac/apis/sign-in-experience';
97
import { getUserInfo } from '@ac/apis/user';
10-
import { sessionExpiredRoute } from '@ac/constants/routes';
8+
import useApi from '@ac/hooks/use-api';
119
import { getThemeBySystemPreference, subscribeToSystemTheme } from '@ac/utils/theme';
1210

1311
import type { PageContextType } from './PageContext';
@@ -23,8 +21,8 @@ type Props = {
2321
};
2422

2523
const PageContextProvider = ({ children }: Props) => {
26-
const { isAuthenticated, getAccessToken } = useLogto();
27-
const navigate = useNavigate();
24+
const { isAuthenticated } = useLogto();
25+
const getUserInfoRequest = useApi(getUserInfo, { silent: true });
2826
const [theme, setTheme] = useState(Theme.Light);
2927
const [toast, setToast] = useState('');
3028
const [experienceSettings, setExperienceSettings] =
@@ -65,30 +63,21 @@ const PageContextProvider = ({ children }: Props) => {
6563
}
6664

6765
const fetchUserInfo = async () => {
68-
try {
69-
const accessToken = await getAccessToken();
70-
if (!accessToken) {
71-
return;
72-
}
73-
74-
const data = await getUserInfo(accessToken);
75-
76-
setUserInfo(data);
77-
setUserInfoError(undefined);
78-
} catch (error: unknown) {
79-
if (error instanceof HTTPError && error.response.status === 401) {
80-
void navigate(sessionExpiredRoute, { replace: true });
81-
return;
82-
}
66+
const [error, data] = await getUserInfoRequest();
8367

68+
if (error || !data) {
8469
setUserInfoError(
8570
error instanceof Error ? error : new Error('Failed to load user information.')
8671
);
72+
return;
8773
}
74+
75+
setUserInfo(data);
76+
setUserInfoError(undefined);
8877
};
8978

9079
void fetchUserInfo();
91-
}, [isAuthenticated, getAccessToken, navigate]);
80+
}, [getUserInfoRequest, isAuthenticated]);
9281

9382
useEffect(() => {
9483
const loadSettings = async () => {
@@ -103,11 +92,6 @@ const PageContextProvider = ({ children }: Props) => {
10392
setAccountCenterSettings(accountCenter);
10493
setExperienceError(undefined);
10594
} catch (error: unknown) {
106-
if (error instanceof HTTPError && error.response.status === 401) {
107-
void navigate(sessionExpiredRoute, { replace: true });
108-
return;
109-
}
110-
11195
setExperienceSettings(undefined);
11296
setAccountCenterSettings(undefined);
11397
setExperienceError(
@@ -119,7 +103,7 @@ const PageContextProvider = ({ children }: Props) => {
119103
};
120104

121105
void loadSettings();
122-
}, [navigate]);
106+
}, []);
123107

124108
useEffect(() => {
125109
if (!experienceSettings?.color.isDarkModeEnabled) {

packages/account-center/src/components/CodeVerification/index.tsx

Lines changed: 3 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ import SmartInputField from '@experience/shared/components/InputFields/SmartInpu
44
import VerificationCodeInput, {
55
defaultLength,
66
} from '@experience/shared/components/VerificationCode';
7-
import { useLogto } from '@logto/react';
87
import { SignInIdentifier } from '@logto/schemas';
98
import type { TFuncKey } from 'i18next';
109
import { useCallback, useContext, useEffect, useState } from 'react';
@@ -60,7 +59,6 @@ const CodeVerification = ({
6059
verifyCode,
6160
}: Props) => {
6261
const { t } = useTranslation();
63-
const { getAccessToken } = useLogto();
6462
const { setToast, setVerificationId } = useContext(PageContext);
6563
const { loading } = useContext(LoadingContext);
6664
const sendCodeRequest = useApi(sendCode);
@@ -104,13 +102,7 @@ const CodeVerification = ({
104102
return;
105103
}
106104

107-
const accessToken = await getAccessToken();
108-
109-
if (!accessToken) {
110-
return;
111-
}
112-
113-
const [error, result] = await sendCodeRequest(accessToken, identifier);
105+
const [error, result] = await sendCodeRequest(identifier);
114106

115107
if (error) {
116108
await handleError(error);
@@ -129,16 +121,7 @@ const CodeVerification = ({
129121
setCodeInput([]);
130122
setHasSentCode(true);
131123
startCountdown();
132-
}, [
133-
getAccessToken,
134-
handleError,
135-
identifier,
136-
loading,
137-
sendCodeRequest,
138-
setToast,
139-
startCountdown,
140-
t,
141-
]);
124+
}, [handleError, identifier, loading, sendCodeRequest, setToast, startCountdown, t]);
142125

143126
const handleVerify = useCallback(
144127
async (code: string[]) => {
@@ -154,13 +137,7 @@ const CodeVerification = ({
154137

155138
const { recordId, expiresAt } = pendingVerificationRecord;
156139

157-
const accessToken = await getAccessToken();
158-
159-
if (!accessToken) {
160-
return;
161-
}
162-
163-
const [error] = await verifyCodeRequest(accessToken, {
140+
const [error] = await verifyCodeRequest({
164141
verificationRecordId: recordId,
165142
code: code.join(''),
166143
identifier,
@@ -176,7 +153,6 @@ const CodeVerification = ({
176153
setPendingVerificationRecord(undefined);
177154
},
178155
[
179-
getAccessToken,
180156
handleError,
181157
identifier,
182158
loading,

packages/account-center/src/components/PasswordVerification/index.tsx

Lines changed: 1 addition & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import Button from '@experience/shared/components/Button';
22
import PasswordInputField from '@experience/shared/components/InputFields/PasswordInputField';
3-
import { useLogto } from '@logto/react';
43
import { useState, useContext, type FormEvent } from 'react';
54
import { useTranslation } from 'react-i18next';
65

@@ -20,7 +19,6 @@ type Props = {
2019
const PasswordVerification = ({ onBack }: Props) => {
2120
const { t } = useTranslation();
2221
const { setVerificationId, setToast } = useContext(PageContext);
23-
const { getAccessToken } = useLogto();
2422
const { loading } = useContext(LoadingContext);
2523
const [password, setPassword] = useState('');
2624
const asyncVerifyPassword = useApi(verifyPassword);
@@ -33,14 +31,7 @@ const PasswordVerification = ({ onBack }: Props) => {
3331
return;
3432
}
3533

36-
const accessToken = await getAccessToken();
37-
38-
if (!accessToken) {
39-
setToast(t('account_center.password_verification.error_failed'));
40-
return;
41-
}
42-
43-
const [error, result] = await asyncVerifyPassword(accessToken, password);
34+
const [error, result] = await asyncVerifyPassword(password);
4435

4536
if (error) {
4637
await handleError(error);
Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
export const sessionExpiredRoute = '/session-expired';
21
export const emailRoute = '/email';
32
export const phoneRoute = '/phone';
43
export const updateSuccessRoute = '/update-success';

packages/account-center/src/hooks/use-api.ts

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { useLogto } from '@logto/react';
12
import type { Nullable } from '@silverhand/essentials';
23
import { useCallback, useContext } from 'react';
34

@@ -8,9 +9,10 @@ type Options = {
89
};
910

1011
const useApi = <Args extends unknown[], Response>(
11-
api: (...args: Args) => Promise<Response>,
12+
api: (accessToken: string, ...args: Args) => Promise<Response>,
1213
options?: Options
1314
) => {
15+
const { getAccessToken } = useLogto();
1416
const { setLoading } = useContext(LoadingContext);
1517

1618
const request = useCallback(
@@ -20,7 +22,13 @@ const useApi = <Args extends unknown[], Response>(
2022
}
2123

2224
try {
23-
const result = await api(...args);
25+
const accessToken = await getAccessToken();
26+
27+
if (!accessToken) {
28+
throw new Error('Session expired');
29+
}
30+
31+
const result = await api(accessToken, ...args);
2432
return [null, result];
2533
} catch (error: unknown) {
2634
return [error];
@@ -30,7 +38,7 @@ const useApi = <Args extends unknown[], Response>(
3038
}
3139
}
3240
},
33-
[api, options?.silent, setLoading]
41+
[api, getAccessToken, options?.silent, setLoading]
3442
);
3543

3644
return request;

0 commit comments

Comments
 (0)