Skip to content

Commit 954ca83

Browse files
authored
feat(account-center): password verification (#7996)
* feat(account-center): restore route after sign-in redirects * feat(account-center): setup account center context * refactor: verification component * feat: fetch account info * feat: add VerificationMethodList * fix: add scopes * refactor: secondary layout * feat: apply layout * feat: verification method button * feat: wrap password verification * refactor: add react router dom * feat: password input * feat: error toast * refactor: simply error * fix: handle go back * feat: createAuthenticatedKy * feat: persist verification id * chore: fix pnpm lock * chore: fix jest scss * fix: add openapi * refactor: update NavBar goback param
1 parent 03bd313 commit 954ca83

File tree

148 files changed

+1205
-155
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

148 files changed

+1205
-155
lines changed

packages/account-center/package.json

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -52,10 +52,12 @@
5252
"react-modal": "^3.15.1",
5353
"react-top-loading-bar": "^2.3.1",
5454
"stylelint": "^15.0.0",
55+
"superstruct": "^2.0.0",
5556
"typescript": "^5.5.3",
5657
"use-debounced-loader": "^0.1.1",
5758
"vite": "^6.3.6",
58-
"vite-plugin-compression": "^0.5.1"
59+
"vite-plugin-compression": "^0.5.1",
60+
"vite-plugin-svgr": "^4.3.0"
5961
},
6062
"engines": {
6163
"node": "^22.14.0"
@@ -66,5 +68,8 @@
6668
"stylelint": {
6769
"extends": "@silverhand/eslint-config-react/.stylelintrc"
6870
},
69-
"prettier": "@silverhand/eslint-config/.prettierrc"
71+
"prettier": "@silverhand/eslint-config/.prettierrc",
72+
"dependencies": {
73+
"react-router-dom": "^7.9.6"
74+
}
7075
}
Lines changed: 36 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
1-
import { LogtoProvider, useLogto } from '@logto/react';
1+
import { LogtoProvider, useLogto, UserScope } from '@logto/react';
22
import { accountCenterApplicationId } from '@logto/schemas';
33
import { useContext, useEffect } from 'react';
4+
import { BrowserRouter, Route, Routes } from 'react-router-dom';
45

56
import AppBoundary from '@ac/Providers/AppBoundary';
67

@@ -11,21 +12,22 @@ import PageContext from './Providers/PageContextProvider/PageContext';
1112
import BrandingHeader from './components/BrandingHeader';
1213
import ErrorPage from './components/ErrorPage';
1314
import initI18n from './i18n/init';
15+
import Email from './pages/Email';
1416
import Home from './pages/Home';
15-
import { handleAccountCenterRoute } from './utils/account-center-route';
17+
import { accountCenterBasePath, handleAccountCenterRoute } from './utils/account-center-route';
1618

1719
import '@experience/shared/scss/normalized.scss';
1820

1921
void initI18n();
2022
handleAccountCenterRoute();
2123

22-
const redirectUri = `${window.location.origin}/account-center`;
24+
const redirectUri = `${window.location.origin}${accountCenterBasePath}`;
2325

2426
const Main = () => {
2527
const params = new URLSearchParams(window.location.search);
2628
const isInCallback = Boolean(params.get('code'));
2729
const { isAuthenticated, isLoading, signIn } = useLogto();
28-
const { isLoadingExperience, experienceError } = useContext(PageContext);
30+
const { isLoadingExperience, experienceError, userInfoError } = useContext(PageContext);
2931

3032
useEffect(() => {
3133
if (isInCallback || isLoading) {
@@ -41,7 +43,7 @@ const Main = () => {
4143
return <Callback />;
4244
}
4345

44-
if (experienceError) {
46+
if (experienceError ?? userInfoError) {
4547
return (
4648
<ErrorPage
4749
titleKey="error.something_went_wrong"
@@ -58,31 +60,40 @@ const Main = () => {
5860
return <div className={styles.status}>Redirecting to sign in…</div>;
5961
}
6062

61-
return <Home />;
63+
return (
64+
<Routes>
65+
<Route path="email" element={<Email />} />
66+
<Route index element={<Home />} />
67+
<Route path="*" element={<Home />} />
68+
</Routes>
69+
);
6270
};
6371

6472
const App = () => (
65-
<LogtoProvider
66-
config={{
67-
endpoint: window.location.origin,
68-
appId: accountCenterApplicationId,
69-
}}
70-
>
71-
<PageContextProvider>
72-
<AppBoundary>
73-
<div className={styles.app}>
74-
<BrandingHeader />
75-
<div className={styles.layout}>
76-
<div className={styles.container}>
77-
<main className={styles.main}>
78-
<Main />
79-
</main>
73+
<BrowserRouter basename={accountCenterBasePath}>
74+
<LogtoProvider
75+
config={{
76+
endpoint: window.location.origin,
77+
appId: accountCenterApplicationId,
78+
scopes: [UserScope.Profile, UserScope.Email, UserScope.Phone, UserScope.Identities],
79+
}}
80+
>
81+
<PageContextProvider>
82+
<AppBoundary>
83+
<div className={styles.app}>
84+
<BrandingHeader />
85+
<div className={styles.layout}>
86+
<div className={styles.container}>
87+
<main className={styles.main}>
88+
<Main />
89+
</main>
90+
</div>
8091
</div>
8192
</div>
82-
</div>
83-
</AppBoundary>
84-
</PageContextProvider>
85-
</LogtoProvider>
93+
</AppBoundary>
94+
</PageContextProvider>
95+
</LogtoProvider>
96+
</BrowserRouter>
8697
);
8798

8899
export default App;

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

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import useColorTheme from '@/Providers/AppBoundary/use-color-theme';
22
import type { ReactElement } from 'react';
33

4+
import ToastProvider from '../ToastProvider';
5+
46
import AppMeta from './AppMeta';
57

68
type Props = {
@@ -13,7 +15,7 @@ const AppBoundary = ({ children }: Props) => {
1315
return (
1416
<>
1517
<AppMeta />
16-
{children}
18+
<ToastProvider>{children}</ToastProvider>
1719
</>
1820
);
1921
};

packages/account-center/src/Providers/ExperiencePageContextBridge.tsx

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,8 @@ type Props = {
1515
* package so shared components (AppBoundary) can consume consistent values.
1616
*/
1717
const ExperiencePageContextBridge = ({ children }: Props) => {
18-
const { theme, setTheme, experienceSettings, setExperienceSettings } =
18+
const { theme, toast, setTheme, setToast, experienceSettings, setExperienceSettings } =
1919
useContext(AccountPageContext);
20-
const [toast, setToast] = useState('');
2120
const [loading, setLoading] = useState(false);
2221
const [platform, setPlatform] = useState<Platform>('web');
2322
const [termsAgreement, setTermsAgreement] = useState(false);

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

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,42 @@
11
import type { SignInExperienceResponse } from '@experience/shared/types';
2-
import { Theme } from '@logto/schemas';
2+
import { Theme, type AccountCenter, type UserProfileResponse } from '@logto/schemas';
33
import { createContext } from 'react';
44

55
// eslint-disable-next-line @typescript-eslint/no-empty-function
66
const noop = () => {};
77

88
export type PageContextType = {
99
theme: Theme;
10+
toast: string;
1011
setTheme: React.Dispatch<React.SetStateAction<Theme>>;
12+
setToast: React.Dispatch<React.SetStateAction<string>>;
1113
experienceSettings?: SignInExperienceResponse;
1214
setExperienceSettings: React.Dispatch<React.SetStateAction<SignInExperienceResponse | undefined>>;
15+
accountCenterSettings?: AccountCenter;
16+
setAccountCenterSettings: React.Dispatch<React.SetStateAction<AccountCenter | undefined>>;
17+
userInfo?: Partial<UserProfileResponse>;
18+
setUserInfo: React.Dispatch<React.SetStateAction<Partial<UserProfileResponse> | undefined>>;
19+
userInfoError?: Error;
20+
verificationId?: string;
21+
setVerificationId: (verificationId?: string, expiresAt?: string) => void;
1322
isLoadingExperience: boolean;
1423
experienceError?: Error;
1524
};
1625

1726
const PageContext = createContext<PageContextType>({
1827
theme: Theme.Light,
28+
toast: '',
1929
setTheme: noop,
30+
setToast: noop,
2031
experienceSettings: undefined,
2132
setExperienceSettings: noop,
33+
accountCenterSettings: undefined,
34+
setAccountCenterSettings: noop,
35+
userInfo: undefined,
36+
setUserInfo: noop,
37+
userInfoError: undefined,
38+
verificationId: undefined,
39+
setVerificationId: noop,
2240
isLoadingExperience: false,
2341
experienceError: undefined,
2442
});

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

Lines changed: 91 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,33 +1,101 @@
1+
import { useLogto } from '@logto/react';
12
import { Theme } from '@logto/schemas';
2-
import { useEffect, useMemo, useState } from 'react';
3+
import { useCallback, useEffect, useMemo, useState } from 'react';
34

5+
import { getAccountCenterSettings } from '@ac/apis/account-center';
46
import { getSignInExperienceSettings } from '@ac/apis/sign-in-experience';
7+
import { getUserInfo } from '@ac/apis/user';
58
import { getThemeBySystemPreference, subscribeToSystemTheme } from '@ac/utils/theme';
69

710
import type { PageContextType } from './PageContext';
811
import PageContext from './PageContext';
12+
import {
13+
clearVerificationRecord,
14+
getStoredVerificationId,
15+
persistVerificationRecord,
16+
} from './verification-storage';
917

1018
type Props = {
1119
readonly children: React.ReactNode;
1220
};
1321

1422
const PageContextProvider = ({ children }: Props) => {
23+
const { isAuthenticated, getAccessToken } = useLogto();
1524
const [theme, setTheme] = useState(Theme.Light);
25+
const [toast, setToast] = useState('');
1626
const [experienceSettings, setExperienceSettings] =
1727
useState<PageContextType['experienceSettings']>(undefined);
28+
const [accountCenterSettings, setAccountCenterSettings] =
29+
useState<PageContextType['accountCenterSettings']>(undefined);
30+
const [userInfo, setUserInfo] = useState<PageContextType['userInfo']>(undefined);
31+
const [userInfoError, setUserInfoError] = useState<Error>();
32+
const [verificationId, setVerificationId] = useState<string>();
1833
const [isLoadingExperience, setIsLoadingExperience] = useState(true);
1934
const [experienceError, setExperienceError] = useState<Error>();
2035

36+
useEffect(() => {
37+
const storedVerificationId = getStoredVerificationId();
38+
39+
if (storedVerificationId) {
40+
setVerificationId(storedVerificationId);
41+
}
42+
}, []);
43+
44+
const setVerificationIdCallback = useCallback((id?: string, expiresAt?: string) => {
45+
setVerificationId(id);
46+
47+
if (!id || !expiresAt) {
48+
clearVerificationRecord();
49+
return;
50+
}
51+
52+
persistVerificationRecord({
53+
verificationId: id,
54+
expiresAt,
55+
});
56+
}, []);
57+
58+
useEffect(() => {
59+
if (!isAuthenticated) {
60+
return;
61+
}
62+
63+
const fetchUserInfo = async () => {
64+
try {
65+
const accessToken = await getAccessToken();
66+
if (!accessToken) {
67+
return;
68+
}
69+
70+
const data = await getUserInfo(accessToken);
71+
72+
setUserInfo(data);
73+
setUserInfoError(undefined);
74+
} catch (error: unknown) {
75+
setUserInfoError(
76+
error instanceof Error ? error : new Error('Failed to load user information.')
77+
);
78+
}
79+
};
80+
81+
void fetchUserInfo();
82+
}, [isAuthenticated, getAccessToken]);
83+
2184
useEffect(() => {
2285
const loadSettings = async () => {
2386
setIsLoadingExperience(true);
2487

2588
try {
26-
const settings = await getSignInExperienceSettings();
89+
const [settings, accountCenter] = await Promise.all([
90+
getSignInExperienceSettings(),
91+
getAccountCenterSettings(),
92+
]);
2793
setExperienceSettings(settings);
94+
setAccountCenterSettings(accountCenter);
2895
setExperienceError(undefined);
2996
} catch (error: unknown) {
3097
setExperienceSettings(undefined);
98+
setAccountCenterSettings(undefined);
3199
setExperienceError(
32100
error instanceof Error ? error : new Error('Failed to load sign-in experience settings.')
33101
);
@@ -60,13 +128,33 @@ const PageContextProvider = ({ children }: Props) => {
60128
const value = useMemo<PageContextType>(
61129
() => ({
62130
theme,
131+
toast,
63132
setTheme,
133+
setToast,
64134
experienceSettings,
65135
setExperienceSettings,
136+
accountCenterSettings,
137+
setAccountCenterSettings,
138+
userInfo,
139+
setUserInfo,
140+
userInfoError,
141+
verificationId,
142+
setVerificationId: setVerificationIdCallback,
66143
isLoadingExperience,
67144
experienceError,
68145
}),
69-
[experienceError, experienceSettings, isLoadingExperience, theme]
146+
[
147+
accountCenterSettings,
148+
experienceError,
149+
experienceSettings,
150+
isLoadingExperience,
151+
theme,
152+
toast,
153+
userInfo,
154+
userInfoError,
155+
verificationId,
156+
setVerificationIdCallback,
157+
]
70158
);
71159

72160
return <PageContext.Provider value={value}>{children}</PageContext.Provider>;

0 commit comments

Comments
 (0)