0
Fork 0
mirror of https://github.com/logto-io/logto.git synced 2025-01-06 20:40:08 -05:00
logto/packages/demo-app/src/App.tsx
2024-07-08 16:52:15 +08:00

193 lines
5.7 KiB
TypeScript

import { type IdTokenClaims, LogtoProvider, useLogto, type Prompt } from '@logto/react';
import { demoAppApplicationId } from '@logto/schemas';
import { useCallback, useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
import '@/scss/normalized.scss';
import * as styles from './App.module.scss';
import Callback from './Callback';
import DevPanel from './DevPanel';
import congratsDark from './assets/congrats-dark.svg';
import congrats from './assets/congrats.svg';
import initI18n from './i18n/init';
import { getLocalData, setLocalData } from './utils';
void initI18n();
const Main = () => {
const config = getLocalData('config');
const params = new URL(window.location.href).searchParams;
const { isAuthenticated, isLoading, getIdTokenClaims, signIn, signOut } = useLogto();
const [user, setUser] = useState<Pick<IdTokenClaims, 'sub' | 'username'>>();
const { t } = useTranslation(undefined, { keyPrefix: 'demo_app' });
const isInCallback = Boolean(params.get('code'));
const isDarkMode = window.matchMedia('(prefers-color-scheme: dark)').matches;
const [congratsIcon, setCongratsIcon] = useState<string>(isDarkMode ? congratsDark : congrats);
const [showDevPanel, setShowDevPanel] = useState(getLocalData('ui').showDevPanel ?? false);
const error = params.get('error');
const errorDescription = params.get('error_description');
const toggleDevPanel = useCallback(() => {
setShowDevPanel((previous) => {
setLocalData('ui', { showDevPanel: !previous });
return !previous;
});
}, []);
useEffect(() => {
if (isInCallback || isLoading || error) {
return;
}
const loadIdTokenClaims = async () => {
const userInfo = await getIdTokenClaims();
setUser(userInfo ?? { sub: 'N/A', username: 'N/A' });
};
// If user is authenticated but user info is not loaded yet, load it
if (isAuthenticated && !user) {
void loadIdTokenClaims();
}
// If user is not authenticated, redirect to sign-in page
if (!isAuthenticated) {
void signIn({
redirectUri: window.location.origin + window.location.pathname,
extraParams: Object.fromEntries(
new URLSearchParams([
...new URLSearchParams(config.signInExtraParams).entries(),
...new URLSearchParams(window.location.search).entries(),
]).entries()
),
});
}
}, [
config.signInExtraParams,
error,
getIdTokenClaims,
isAuthenticated,
isInCallback,
isLoading,
signIn,
user,
]);
useEffect(() => {
const onThemeChange = (event: MediaQueryListEvent) => {
const isDarkMode = event.matches;
setCongratsIcon(isDarkMode ? congratsDark : congrats);
};
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', onThemeChange);
return () => {
window
.matchMedia('(prefers-color-scheme: dark)')
.removeEventListener('change', onThemeChange);
};
}, []);
if (isInCallback) {
return <Callback />;
}
if (error) {
return (
<div className={styles.app}>
<div className={styles.error}>
<p>
Error occurred: {error}
<br />
{errorDescription}
</p>
<button
className={styles.button}
onClick={() => {
setLocalData('config', {});
window.location.assign('/demo-app');
}}
>
Reset config and retry
</button>
</div>
</div>
);
}
if (!isAuthenticated || !user) {
return null;
}
return (
<div className={styles.app}>
{showDevPanel && <DevPanel />}
<div className={[styles.card, styles.congrats].join(' ')}>
{congratsIcon && <img src={congratsIcon} alt="Congrats" />}
<div className={styles.title}>{t('title')}</div>
<div className={styles.text}>{t('subtitle')}</div>
<div className={styles.infoCard}>
{user.username && (
<div>
{t('username')}
<span>{user.username}</span>
</div>
)}
<div>
{t('user_id')}
<span>{user.sub}</span>
</div>
</div>
<div
role="button"
tabIndex={0}
className={styles.button}
onClick={async () => signOut(`${window.location.origin}/demo-app`)}
onKeyDown={({ key }) => {
if (key === 'Enter' || key === ' ') {
void signOut(`${window.location.origin}/demo-app`);
}
}}
>
{t('sign_out')}
</div>
<div
role="button"
tabIndex={0}
className={styles.button}
onClick={toggleDevPanel}
onKeyDown={({ key }) => {
if (key === 'Enter' || key === ' ') {
toggleDevPanel();
}
}}
>
{showDevPanel ? 'Close' : 'Open'} dev panel
</div>
</div>
</div>
);
};
const App = () => {
const params = new URL(window.location.href).searchParams;
const config = getLocalData('config');
return (
<LogtoProvider
config={{
endpoint: window.location.origin,
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing -- We need to fall back for empty string
appId: params.get('app_id') || config.appId || demoAppApplicationId,
// eslint-disable-next-line no-restricted-syntax
prompt: config.prompt ? (config.prompt.split(' ') as Prompt[]) : [],
scopes: config.scope ? config.scope.split(' ') : [],
resources: config.resource ? config.resource.split(' ') : [],
}}
>
<Main />
</LogtoProvider>
);
};
export default App;