Skip to content

Commit d35c8cf

Browse files
authored
refactor(account-center): use global loading layer (#8002)
1 parent 6be69a0 commit d35c8cf

File tree

20 files changed

+168
-77
lines changed

20 files changed

+168
-77
lines changed

packages/account-center/src/App.tsx

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

66
import AppBoundary from '@ac/Providers/AppBoundary';
7+
import LoadingContextProvider from '@ac/Providers/LoadingContextProvider';
78

89
import styles from './App.module.scss';
910
import Callback from './Callback';
@@ -79,20 +80,22 @@ const App = () => (
7980
scopes: [UserScope.Profile, UserScope.Email, UserScope.Phone, UserScope.Identities],
8081
}}
8182
>
82-
<PageContextProvider>
83-
<AppBoundary>
84-
<div className={styles.app}>
85-
<BrandingHeader />
86-
<div className={styles.layout}>
87-
<div className={styles.container}>
88-
<main className={styles.main}>
89-
<Main />
90-
</main>
83+
<LoadingContextProvider>
84+
<PageContextProvider>
85+
<AppBoundary>
86+
<div className={styles.app}>
87+
<BrandingHeader />
88+
<div className={styles.layout}>
89+
<div className={styles.container}>
90+
<main className={styles.main}>
91+
<Main />
92+
</main>
93+
</div>
9194
</div>
9295
</div>
93-
</div>
94-
</AppBoundary>
95-
</PageContextProvider>
96+
</AppBoundary>
97+
</PageContextProvider>
98+
</LoadingContextProvider>
9699
</LogtoProvider>
97100
</BrowserRouter>
98101
);
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import { noop } from '@silverhand/essentials';
2+
import { createContext } from 'react';
3+
4+
export type LoadingContextValue = {
5+
loading: boolean;
6+
setLoading: React.Dispatch<React.SetStateAction<boolean>>;
7+
};
8+
9+
const LoadingContext = createContext<LoadingContextValue>({
10+
loading: false,
11+
setLoading: noop,
12+
});
13+
14+
export default LoadingContext;
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import { useMemo, useState, type ReactNode } from 'react';
2+
3+
import Loading from '@ac/components/Loading';
4+
5+
import LoadingContext from './LoadingContext';
6+
7+
type Props = {
8+
readonly children: ReactNode;
9+
};
10+
11+
const LoadingContextProvider = ({ children }: Props) => {
12+
const [loading, setLoading] = useState(false);
13+
14+
const value = useMemo(
15+
() => ({
16+
loading,
17+
setLoading,
18+
}),
19+
[loading]
20+
);
21+
22+
return (
23+
<LoadingContext.Provider value={value}>
24+
{children}
25+
{loading && <Loading />}
26+
</LoadingContext.Provider>
27+
);
28+
};
29+
30+
export default LoadingContextProvider;

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

Lines changed: 36 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,9 @@ import type { TFuncKey } from 'i18next';
1010
import { useCallback, useContext, useEffect, useState } from 'react';
1111
import { useTranslation } from 'react-i18next';
1212

13+
import LoadingContext from '@ac/Providers/LoadingContextProvider/LoadingContext';
1314
import PageContext from '@ac/Providers/PageContextProvider/PageContext';
15+
import useApi from '@ac/hooks/use-api';
1416
import SecondaryPageLayout from '@ac/layouts/SecondaryPageLayout';
1517

1618
import styles from './index.module.scss';
@@ -59,10 +61,11 @@ const CodeVerification = ({
5961
const { t } = useTranslation();
6062
const { getAccessToken } = useLogto();
6163
const { setToast, setVerificationId } = useContext(PageContext);
64+
const { loading } = useContext(LoadingContext);
65+
const sendCodeRequest = useApi(sendCode);
66+
const verifyCodeRequest = useApi(verifyCode);
6267
const [codeInput, setCodeInput] = useState<string[]>([]);
6368
const [errorMessage, setErrorMessage] = useState<string>();
64-
const [isSending, setIsSending] = useState(false);
65-
const [isVerifying, setIsVerifying] = useState(false);
6669
const [countdown, setCountdown] = useState(0);
6770
const [pendingVerificationRecord, setPendingVerificationRecord] = useState<{
6871
recordId: string;
@@ -96,7 +99,7 @@ const CodeVerification = ({
9699
}, [countdown]);
97100

98101
const handleSendCode = useCallback(async () => {
99-
if (!identifier || isSending) {
102+
if (!identifier || loading) {
100103
return;
101104
}
102105

@@ -106,33 +109,30 @@ const CodeVerification = ({
106109
return;
107110
}
108111

109-
setIsSending(true);
112+
const [error, result] = await sendCodeRequest(accessToken, identifier);
110113

111-
try {
112-
const result = await sendCode(accessToken, identifier);
113-
114-
setPendingVerificationRecord({
115-
recordId: result.verificationRecordId,
116-
expiresAt: result.expiresAt,
117-
});
118-
setCodeInput([]);
119-
setErrorMessage(undefined);
120-
setHasSentCode(true);
121-
startCountdown();
122-
} catch {
114+
if (error || !result) {
123115
setToast(t('account_center.email_verification.error_send_failed'));
124-
} finally {
125-
setIsSending(false);
116+
return;
126117
}
127-
}, [getAccessToken, identifier, isSending, sendCode, setToast, startCountdown, t]);
118+
119+
setPendingVerificationRecord({
120+
recordId: result.verificationRecordId,
121+
expiresAt: result.expiresAt,
122+
});
123+
setCodeInput([]);
124+
setErrorMessage(undefined);
125+
setHasSentCode(true);
126+
startCountdown();
127+
}, [getAccessToken, identifier, loading, sendCodeRequest, setToast, startCountdown, t]);
128128

129129
const handleVerify = useCallback(
130130
async (code: string[]) => {
131131
if (
132132
!identifier ||
133133
!pendingVerificationRecord?.recordId ||
134134
!pendingVerificationRecord.expiresAt ||
135-
isVerifying ||
135+
loading ||
136136
!isCodeReady(code)
137137
) {
138138
return;
@@ -146,32 +146,29 @@ const CodeVerification = ({
146146
return;
147147
}
148148

149-
setIsVerifying(true);
150-
151-
try {
152-
await verifyCode(accessToken, {
153-
verificationRecordId: recordId,
154-
code: code.join(''),
155-
identifier,
156-
});
149+
const [error] = await verifyCodeRequest(accessToken, {
150+
verificationRecordId: recordId,
151+
code: code.join(''),
152+
identifier,
153+
});
157154

158-
setVerificationId(recordId, expiresAt);
159-
setPendingVerificationRecord(undefined);
160-
} catch {
155+
if (error) {
161156
setCodeInput([]);
162157
setErrorMessage(t('account_center.email_verification.error_verify_failed'));
163-
} finally {
164-
setIsVerifying(false);
158+
return;
165159
}
160+
161+
setVerificationId(recordId, expiresAt);
162+
setPendingVerificationRecord(undefined);
166163
},
167164
[
168165
getAccessToken,
169166
identifier,
170-
isVerifying,
167+
loading,
171168
pendingVerificationRecord,
172169
setVerificationId,
173170
t,
174-
verifyCode,
171+
verifyCodeRequest,
175172
]
176173
);
177174

@@ -225,7 +222,7 @@ const CodeVerification = ({
225222
<button
226223
className={styles.resendButton}
227224
type="button"
228-
disabled={isSending}
225+
disabled={loading}
229226
onClick={() => {
230227
void handleSendCode();
231228
}}
@@ -238,7 +235,7 @@ const CodeVerification = ({
238235
title="action.confirm"
239236
type="primary"
240237
className={styles.submit}
241-
isLoading={isVerifying}
238+
isLoading={loading}
242239
onClick={() => {
243240
if (!isCodeReady(codeInput)) {
244241
setErrorMessage(t('error.invalid_passcode'));
@@ -263,7 +260,8 @@ const CodeVerification = ({
263260
title="account_center.email_verification.send"
264261
type="primary"
265262
className={styles.prepareAction}
266-
isLoading={isSending}
263+
disabled={loading}
264+
isLoading={loading}
267265
onClick={() => {
268266
void handleSendCode();
269267
}}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import LoadingLayer from '@experience/shared/components/LoadingLayer';
2+
3+
const Loading = () => <LoadingLayer />;
4+
5+
export default Loading;

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

Lines changed: 18 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,10 @@ import { useLogto } from '@logto/react';
44
import { useState, useContext, type FormEvent } from 'react';
55
import { useTranslation } from 'react-i18next';
66

7+
import LoadingContext from '@ac/Providers/LoadingContextProvider/LoadingContext';
78
import PageContext from '@ac/Providers/PageContextProvider/PageContext';
89
import { verifyPassword } from '@ac/apis/verification';
10+
import useApi from '@ac/hooks/use-api';
911
import SecondaryPageLayout from '@ac/layouts/SecondaryPageLayout';
1012

1113
import styles from './index.module.scss';
@@ -18,32 +20,32 @@ const PasswordVerification = ({ onBack }: Props) => {
1820
const { t } = useTranslation();
1921
const { setVerificationId, setToast } = useContext(PageContext);
2022
const { getAccessToken } = useLogto();
23+
const { loading } = useContext(LoadingContext);
2124
const [password, setPassword] = useState('');
22-
const [loading, setLoading] = useState(false);
25+
const asyncVerifyPassword = useApi(verifyPassword);
2326

2427
const handleVerify = async (event?: FormEvent<HTMLFormElement>) => {
2528
event?.preventDefault();
2629

27-
if (!password) {
30+
if (!password || loading) {
2831
return;
2932
}
3033

31-
setLoading(true);
32-
try {
33-
const accessToken = await getAccessToken();
34+
const accessToken = await getAccessToken();
3435

35-
if (!accessToken) {
36-
throw new Error('Missing access token');
37-
}
36+
if (!accessToken) {
37+
setToast(t('account_center.password_verification.error_failed'));
38+
return;
39+
}
40+
41+
const [error, result] = await asyncVerifyPassword(accessToken, password);
3842

39-
const result = await verifyPassword(accessToken, password);
40-
setVerificationId(result.verificationRecordId, result.expiresAt);
41-
} catch {
42-
const errorMessage = t('account_center.password_verification.error_failed');
43-
setToast(errorMessage);
44-
} finally {
45-
setLoading(false);
43+
if (error || !result) {
44+
setToast(t('account_center.password_verification.error_failed'));
45+
return;
4646
}
47+
48+
setVerificationId(result.verificationRecordId, result.expiresAt);
4749
};
4850

4951
return (
@@ -67,7 +69,7 @@ const PasswordVerification = ({ onBack }: Props) => {
6769
className={styles.submit}
6870
htmlType="submit"
6971
title="action.confirm"
70-
disabled={!password}
72+
disabled={!password || loading}
7173
isLoading={loading}
7274
/>
7375
</form>
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import type { Nullable } from '@silverhand/essentials';
2+
import { useCallback, useContext } from 'react';
3+
4+
import LoadingContext from '@ac/Providers/LoadingContextProvider/LoadingContext';
5+
6+
type Options = {
7+
silent?: boolean;
8+
};
9+
10+
const useApi = <Args extends unknown[], Response>(
11+
api: (...args: Args) => Promise<Response>,
12+
options?: Options
13+
) => {
14+
const { setLoading } = useContext(LoadingContext);
15+
16+
const request = useCallback(
17+
async (...args: Args): Promise<[Nullable<unknown>, Response?]> => {
18+
if (!options?.silent) {
19+
setLoading(true);
20+
}
21+
22+
try {
23+
const result = await api(...args);
24+
return [null, result];
25+
} catch (error: unknown) {
26+
return [error];
27+
} finally {
28+
if (!options?.silent) {
29+
setLoading(false);
30+
}
31+
}
32+
},
33+
[api, options?.silent, setLoading]
34+
);
35+
36+
return request;
37+
};
38+
39+
export default useApi;

packages/experience/src/Providers/LoadingLayerProvider/index.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { useContext } from 'react';
22
import { Outlet } from 'react-router-dom';
33

44
import PageContext from '@/Providers/PageContextProvider/PageContext';
5-
import LoadingMask from '@/components/LoadingMask';
5+
import { LoadingMask } from '@/shared/components/LoadingLayer';
66

77
const LoadingLayerProvider = () => {
88
const { loading } = useContext(PageContext);

packages/experience/src/containers/SocialLanding/index.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import classNames from 'classnames';
22

3-
import { LoadingIcon } from '@/components/LoadingLayer';
43
import useConnectors from '@/hooks/use-connectors';
4+
import { LoadingIcon } from '@/shared/components/LoadingLayer';
55

66
import styles from './index.module.scss';
77

packages/experience/src/pages/DirectSignIn/index.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,11 @@ import { useContext, useEffect } from 'react';
33
import { useParams } from 'react-router-dom';
44

55
import PageContext from '@/Providers/PageContextProvider/PageContext';
6-
import { LoadingIconWithContainer } from '@/components/LoadingLayer';
76
import useSocial from '@/containers/SocialSignInList/use-social';
87
import useFallbackRoute from '@/hooks/use-fallback-route';
98
import { useSieMethods } from '@/hooks/use-sie';
109
import useSingleSignOn from '@/hooks/use-single-sign-on';
10+
import { LoadingIconWithContainer } from '@/shared/components/LoadingLayer';
1111
import { logtoGoogleOneTapCookie } from '@/utils/cookies';
1212

1313
import styles from './index.module.scss';

0 commit comments

Comments
 (0)