0
Fork 0
mirror of https://github.com/logto-io/logto.git synced 2024-12-16 20:26:19 -05:00

refactor(console): reorg hooks and data checking logics

This commit is contained in:
Gao Sun 2023-06-24 17:23:41 +08:00
parent a3e3363b10
commit d7f96a6559
No known key found for this signature in database
GPG key ID: 13EBE123E4773688
18 changed files with 250 additions and 229 deletions

View file

@ -9,7 +9,7 @@
"prepare": "if test \"$NODE_ENV\" != \"production\" && test \"$CI\" != \"true\" ; then husky install ; fi",
"prepack": "pnpm -r prepack",
"dev": "pnpm -r prepack && pnpm start:dev",
"dev:cloud": "pnpm -r prepack && pnpm start:dev:cloud",
"dev:cloud": "IS_CLOUD=1 pnpm -r prepack && pnpm start:dev:cloud",
"start:dev": "pnpm -r --parallel --filter=!@logto/integration-tests --filter \"!./packages/connectors/connector-*\" --filter=!@logto/cloud dev",
"start:dev:cloud": "CONSOLE_PUBLIC_URL=/ IS_CLOUD=1 pnpm -r --parallel --filter=!@logto/integration-tests --filter \"!./packages/connectors/connector-*\" dev",
"start": "cd packages/core && NODE_ENV=production node .",

View file

@ -15,7 +15,6 @@ import '@fontsource/roboto-mono';
import CloudApp from '@/cloud/App';
import { cloudApi, getManagementApi, meApi } from '@/consts/resources';
import initI18n from '@/i18n/init';
import { adminTenantEndpoint, mainTitle } from './consts';
import { isCloud } from './consts/env';
@ -25,11 +24,12 @@ import AppConfirmModalProvider from './contexts/AppConfirmModalProvider';
import AppEndpointsProvider from './contexts/AppEndpointsProvider';
import { AppThemeProvider } from './contexts/AppThemeProvider';
import TenantsProvider, { TenantsContext } from './contexts/TenantsProvider';
import initI18n from './i18n/init';
void initI18n();
function Content() {
const { tenants, isInitComplete, currentTenantId } = useContext(TenantsContext);
const { tenants, currentTenantId } = useContext(TenantsContext);
const resources = useMemo(
() =>
@ -80,7 +80,7 @@ function Content() {
* If it's not Cloud (OSS), render the tenant app container directly since only default tenant is available;
* if it's Cloud, render the tenant app container only when init is complete and a tenant ID is available (in a tenant context).
*/}
{!isCloud || (isInitComplete && currentTenantId) ? (
{!isCloud || currentTenantId ? (
<AppEndpointsProvider>
<AppConfirmModalProvider>
<TenantAppContainer />

View file

@ -1,21 +1,23 @@
import { BrowserRouter, Route, Routes } from 'react-router-dom';
import ProtectedContent from '@/containers/ProtectedRoutes';
import Callback from '@cloud/pages/Callback';
import * as styles from './App.module.scss';
import Main from './pages/Main';
import SocialDemoCallback from './pages/SocialDemoCallback';
import { CloudRoute } from './types';
function App() {
return (
<BrowserRouter>
<div className={styles.app}>
<Routes>
<Route path={`/${CloudRoute.Callback}`} element={<Callback />} />
<Route path={`/${CloudRoute.SocialDemoCallback}`} element={<SocialDemoCallback />} />
<Route path={`/:tenantId/${CloudRoute.Callback}`} element={<Callback />} />
<Route path="/*" element={<Main />} />
<Route path="/callback" element={<Callback />} />
<Route path="/social-demo-callback" element={<SocialDemoCallback />} />
<Route path="/:tenantId/callback" element={<Callback />} />
<Route path="/*" element={<ProtectedContent />}>
<Route path="*" element={<Main />} />
</Route>
</Routes>
</div>
</BrowserRouter>

View file

@ -1,11 +1,5 @@
import { useLogto } from '@logto/react';
import { conditional, yes } from '@silverhand/essentials';
import { useContext, useEffect } from 'react';
import { useHref, useSearchParams } from 'react-router-dom';
import { useContext } from 'react';
import { useCloudApi } from '@/cloud/hooks/use-cloud-api';
import AppLoading from '@/components/AppLoading';
import { searchKeys } from '@/consts';
import { TenantsContext } from '@/contexts/TenantsProvider';
import useUserOnboardingData from '@/onboarding/hooks/use-user-onboarding-data';
@ -13,34 +7,15 @@ import AutoCreateTenant from './AutoCreateTenant';
import Redirect from './Redirect';
import TenantLandingPage from './TenantLandingPage';
function Content() {
const api = useCloudApi();
const { tenants, resetTenants, currentTenantId, isInitComplete } = useContext(TenantsContext);
const { isOnboarding, isLoaded: isOnboardingDataLoaded } = useUserOnboardingData();
// Load tenants from the cloud API for the first render.
useEffect(() => {
const loadTenants = async () => {
const data = await api.get('/api/tenants');
resetTenants(data);
};
if (!isInitComplete) {
void loadTenants();
}
}, [api, resetTenants, tenants, isInitComplete]);
if (!isInitComplete || !isOnboardingDataLoaded) {
return <AppLoading />;
}
export default function Main() {
const { tenants } = useContext(TenantsContext);
const { isOnboarding } = useUserOnboardingData();
/**
* Trigger a redirect when one of the following conditions is met:
*
* - If a current tenant ID has been set in the URL; or
* - If current tenant ID is not set, but there is at least one tenant available.
* If current tenant ID is not set, but there is at least one tenant available,
* trigger a redirect to the first tenant.
*/
if (currentTenantId || tenants[0]) {
if (tenants[0]) {
return <Redirect />;
}
@ -52,28 +27,3 @@ function Content() {
// If user has completed onboarding and still has no tenant, redirect to a special landing page.
return <TenantLandingPage />;
}
function Main() {
const [searchParameters] = useSearchParams();
const { isAuthenticated, isLoading, signIn } = useLogto();
const { currentTenantId } = useContext(TenantsContext);
const href = useHref(currentTenantId + '/callback');
useEffect(() => {
if (!isLoading && !isAuthenticated) {
const isSignUpMode = yes(searchParameters.get(searchKeys.signUp));
void signIn(
new URL(href, window.location.origin).toString(),
conditional(isSignUpMode && 'signUp')
);
}
}, [href, isAuthenticated, isLoading, searchParameters, signIn]);
if (!isAuthenticated) {
return <AppLoading />;
}
return <Content />;
}
export default Main;

View file

@ -1,4 +0,0 @@
export enum CloudRoute {
Callback = 'callback',
SocialDemoCallback = 'social-demo-callback',
}

View file

@ -1,6 +1,7 @@
import { useLogto } from '@logto/react';
import { useTranslation } from 'react-i18next';
import { getCallbackUrl } from '@/consts';
import Button from '@/ds-components/Button';
import AppError from '../AppError';
@ -9,10 +10,9 @@ import * as styles from './index.module.scss';
type Props = {
error: Error;
callbackHref?: string;
};
function SessionExpired({ callbackHref = '/callback', error }: Props) {
function SessionExpired({ error }: Props) {
const { signIn } = useLogto();
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' });
@ -28,7 +28,7 @@ function SessionExpired({ callbackHref = '/callback', error }: Props) {
type="outline"
title="session_expired.button"
onClick={() => {
void signIn(new URL(callbackHref, window.location.origin).toString());
void signIn(getCallbackUrl().href);
}}
/>
</AppError>

View file

@ -1,8 +1,6 @@
import { defaultTenantId, ossConsolePath } from '@logto/schemas';
import { conditionalArray } from '@silverhand/essentials';
import { CloudRoute } from '@/cloud/types';
import { adminEndpoint, isCloud } from './env';
const getAdminTenantEndpoint = () => {
@ -24,12 +22,11 @@ export const getUserTenantId = () => {
if (isCloud) {
const segment = window.location.pathname.split('/')[1];
// eslint-disable-next-line no-restricted-syntax
if (Object.values(CloudRoute).includes(segment as CloudRoute)) {
if (!segment || segment === 'callback' || segment.endsWith('-callback')) {
return '';
}
return segment ?? '';
return segment;
}
return defaultTenantId;
@ -38,7 +35,11 @@ export const getUserTenantId = () => {
export const getBasename = () => (isCloud ? '/' + getUserTenantId() : ossConsolePath);
export const getCallbackUrl = (tenantId?: string) =>
new URL('/' + conditionalArray(tenantId, 'callback').join('/'), window.location.origin);
new URL(
// Only Cloud has tenantId in callback URL
'/' + conditionalArray(isCloud ? tenantId : 'console', 'callback').join('/'),
window.location.origin
);
export const getSignOutRedirectPathname = () => (isCloud ? '/' : ossConsolePath);

View file

@ -2,7 +2,7 @@ import { LogtoClientError, LogtoError, useLogto } from '@logto/react';
import { conditional } from '@silverhand/essentials';
import { useEffect, useRef } from 'react';
import { useTranslation } from 'react-i18next';
import { Outlet, useHref, useLocation, useNavigate } from 'react-router-dom';
import { Outlet, useLocation, useNavigate } from 'react-router-dom';
import AppError from '@/components/AppError';
import AppLoading from '@/components/AppLoading';
@ -11,6 +11,7 @@ import { isCloud } from '@/consts/env';
import useConfigs from '@/hooks/use-configs';
import useScroll from '@/hooks/use-scroll';
import useUserPreferences from '@/hooks/use-user-preferences';
import useValidateTenantAccess from '@/hooks/use-validate-tenant-access';
import Broadcast from '@/onboarding/components/Broadcast';
import { getPath } from '../ConsoleContent/Sidebar';
@ -21,8 +22,7 @@ import * as styles from './index.module.scss';
import { type AppContentOutletContext } from './types';
function AppContent() {
const { isAuthenticated, isLoading: isLogtoLoading, error, signIn } = useLogto();
const href = useHref('/callback');
const { error } = useLogto();
const { isLoading: isPreferencesLoading } = useUserPreferences();
const { isLoading: isConfigsLoading } = useConfigs();
@ -35,11 +35,7 @@ function AppContent() {
const { scrollTop } = useScroll(scrollableContent.current);
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' });
useEffect(() => {
if (!isAuthenticated && !isLogtoLoading) {
void signIn(new URL(href, window.location.origin).toString());
}
}, [href, isAuthenticated, isLogtoLoading, signIn]);
useValidateTenantAccess();
useEffect(() => {
// Navigate to the first menu item after configs are loaded.
@ -48,9 +44,13 @@ function AppContent() {
}
}, [firstItem?.title, isLoading, location.pathname, navigate]);
if (isLoading) {
return <AppLoading />;
}
if (error) {
if (error instanceof LogtoClientError) {
return <SessionExpired error={error} callbackHref={href} />;
return <SessionExpired error={error} />;
}
if (error instanceof LogtoError && error.code === 'crypto_subtle_unavailable') {
@ -60,10 +60,6 @@ function AppContent() {
return <AppError errorMessage={error.message} callStack={error.stack} />;
}
if (!isAuthenticated || isLoading) {
return <AppLoading />;
}
return (
<>
<div className={styles.app}>

View file

@ -0,0 +1,42 @@
import { useLogto } from '@logto/react';
import { yes, conditional } from '@silverhand/essentials';
import { useContext, useEffect, useState } from 'react';
import { Outlet, useSearchParams } from 'react-router-dom';
import { useCloudApi } from '@/cloud/hooks/use-cloud-api';
import AppLoading from '@/components/AppLoading';
import { searchKeys, getCallbackUrl } from '@/consts';
import { TenantsContext } from '@/contexts/TenantsProvider';
export default function ProtectedRoutes() {
const api = useCloudApi();
const [searchParameters] = useSearchParams();
const { isAuthenticated, isLoading, signIn } = useLogto();
const { currentTenantId, isInitComplete, resetTenants } = useContext(TenantsContext);
const [loadingTenants, setLoadingTenants] = useState(false);
useEffect(() => {
if (!isLoading && !isAuthenticated) {
const isSignUpMode = yes(searchParameters.get(searchKeys.signUp));
void signIn(getCallbackUrl(currentTenantId).href, conditional(isSignUpMode && 'signUp'));
}
}, [currentTenantId, isAuthenticated, isLoading, searchParameters, signIn]);
useEffect(() => {
if (isAuthenticated && !loadingTenants && !isInitComplete) {
const loadTenants = async () => {
const data = await api.get('/api/tenants');
resetTenants(data);
};
setLoadingTenants(true);
void loadTenants();
}
}, [api, isAuthenticated, isInitComplete, loadingTenants, resetTenants]);
if (isLoading || !isInitComplete) {
return <AppLoading />;
}
return <Outlet />;
}

View file

@ -1,38 +1,35 @@
import { useLogto } from '@logto/react';
import { type TenantInfo } from '@logto/schemas/lib/models/tenants.js';
import { trySafe } from '@silverhand/essentials';
import { useContext, useEffect } from 'react';
import { useContext, useMemo } from 'react';
import { RouterProvider, createBrowserRouter } from 'react-router-dom';
import AppLoading from '@/components/AppLoading';
import { getCallbackUrl } from '@/consts';
import { getBasename } from '@/consts';
import { isCloud } from '@/consts/env';
import { AppEndpointsContext } from '@/contexts/AppEndpointsProvider';
import { TenantsContext } from '@/contexts/TenantsProvider';
import useTrackUserId from '@/hooks/use-track-user-id';
import OnboardingApp from '@/onboarding/App';
import { OnboardingRoutes } from '@/onboarding';
import useUserOnboardingData from '@/onboarding/hooks/use-user-onboarding-data';
import ConsoleApp from '@/pages/Main';
import { ConsoleRoutes } from '@/pages/ConsoleRoutes';
function TenantAppContainer() {
const { getAccessToken, signIn } = useLogto();
const { userEndpoint } = useContext(AppEndpointsContext);
const { isOnboarding, isLoaded } = useUserOnboardingData();
const { currentTenant } = useContext(TenantsContext);
useEffect(() => {
const validate = async ({ indicator, id }: TenantInfo) => {
// Test fetching an access token for the current Tenant ID.
// If failed, it means the user finishes the first auth, ands still needs to auth again to
// fetch the full-scoped (with all available tenants) token.
if (!(await trySafe(getAccessToken(indicator)))) {
void signIn(getCallbackUrl(id).href);
}
};
if (currentTenant) {
void validate(currentTenant);
}
}, [currentTenant, getAccessToken, signIn]);
const router = useMemo(
() =>
createBrowserRouter(
[{ path: '*', Component: isOnboarding ? OnboardingRoutes : ConsoleRoutes }],
// Currently we use `window.open()` to navigate between tenants so the `useMemo` hook
// can have no dependency and the router will be created anyway. Consider integrating the
// tenant ID into the router and remove basename here if we want to use `history.pushState()`
// to navigate.
//
// Caveat: To use `history.pushState()`, we'd better to create a browser router in the upper
// level of the component tree to make the tenant ID a part of the URL. Otherwise, we need
// to handle `popstate` event to update the tenant ID when the user navigates back.
{ basename: getBasename() }
),
[isOnboarding]
);
useTrackUserId();
@ -40,11 +37,7 @@ function TenantAppContainer() {
return <AppLoading />;
}
if (isOnboarding) {
return <OnboardingApp />;
}
return <ConsoleApp />;
return <RouterProvider router={router} />;
}
export default TenantAppContainer;

View file

@ -32,7 +32,9 @@ type Tenants = {
*/
currentTenantId: string;
currentTenant?: TenantInfo;
setCurrentTenantId: (tenantId: string) => void;
/** Indicates if the Access Token has been validated for use. Will be reset to false when the current tenant changes. */
currentTenantValidated: boolean;
setCurrentTenantValidated: () => void;
navigateTenant: (tenantId: string, options?: NavigateOptions) => void;
};
@ -56,7 +58,8 @@ export const TenantsContext = createContext<Tenants>({
removeTenant: noop,
updateTenant: noop,
currentTenantId: '',
setCurrentTenantId: noop,
currentTenantValidated: false,
setCurrentTenantValidated: noop,
navigateTenant: noop,
});
@ -75,12 +78,14 @@ function TenantsProvider({ children }: Props) {
/** @see {@link initialTenants} */
const [isInitComplete, setIsInitComplete] = useState(!isCloud);
const [currentTenantId, setCurrentTenantId] = useState(getUserTenantId());
const [currentTenantValidated, setCurrentTenantValidated] = useState(false);
const navigateTenant = useCallback((tenantId: string) => {
// Use `window.open()` to force page reload since we use `basename` for the router
// which will not re-create the router instance when the URL changes.
window.open(`/${tenantId}`, '_self');
setCurrentTenantId(tenantId);
setCurrentTenantValidated(false);
}, []);
const currentTenant = useMemo(
@ -109,10 +114,20 @@ function TenantsProvider({ children }: Props) {
isInitComplete,
currentTenantId,
currentTenant,
setCurrentTenantId,
currentTenantValidated,
setCurrentTenantValidated: () => {
setCurrentTenantValidated(true);
},
navigateTenant,
}),
[currentTenant, currentTenantId, isInitComplete, navigateTenant, tenants]
[
currentTenant,
currentTenantId,
currentTenantValidated,
isInitComplete,
navigateTenant,
tenants,
]
);
return <TenantsContext.Provider value={memorizedContext}>{children}</TenantsContext.Provider>;

View file

@ -1,4 +1,6 @@
import { useLogto } from '@logto/react';
import { isKeyInObject } from '@logto/shared/universal';
import { conditional } from '@silverhand/essentials';
import { useCallback } from 'react';
import { toast } from 'react-hot-toast';
import { useTranslation } from 'react-i18next';
@ -20,10 +22,13 @@ const useMeCustomData = () => {
const fetcher = useSwrFetcher(api);
const { data, mutate, error } = useSWR<unknown, RequestError>(
shouldFetch && `me/custom-data`,
fetcher
);
const {
data: meData,
mutate,
error,
// Reuse the same key `me` as `useCurrentUser()` to avoid additional requests.
} = useSWR<unknown, RequestError>(shouldFetch && 'me', fetcher);
const data = conditional(isKeyInObject(meData, 'customData') && meData.customData);
const update = useCallback(
async (data: Record<string, unknown>) => {

View file

@ -0,0 +1,38 @@
import { useLogto } from '@logto/react';
import { type TenantInfo } from '@logto/schemas/lib/models/tenants.js';
import { trySafe } from '@silverhand/essentials';
import { useContext, useEffect } from 'react';
import { getCallbackUrl } from '@/consts';
import { TenantsContext } from '@/contexts/TenantsProvider';
const useValidateTenantAccess = () => {
const { getAccessToken, signIn, isAuthenticated } = useLogto();
const { currentTenant, currentTenantValidated, setCurrentTenantValidated } =
useContext(TenantsContext);
useEffect(() => {
const validate = async ({ indicator, id }: TenantInfo) => {
// Test fetching an access token for the current Tenant ID.
// If failed, it means the user finishes the first auth, ands still needs to auth again to
// fetch the full-scoped (with all available tenants) token.
if (!(await trySafe(getAccessToken(indicator)))) {
void signIn(getCallbackUrl(id).href);
}
};
if (isAuthenticated && currentTenant && !currentTenantValidated) {
setCurrentTenantValidated();
void validate(currentTenant);
}
}, [
currentTenant,
currentTenantValidated,
getAccessToken,
isAuthenticated,
setCurrentTenantValidated,
signIn,
]);
};
export default useValidateTenantAccess;

View file

@ -3,21 +3,21 @@ import { TrackOnce } from '@logto/app-insights/react';
import { Theme } from '@logto/schemas';
import { useContext, useEffect } from 'react';
import { Helmet } from 'react-helmet';
import { BrowserRouter, Routes, Route, Navigate } from 'react-router-dom';
import { Route, Navigate, Outlet, useLocation, Routes } from 'react-router-dom';
import { SWRConfig } from 'swr';
import AppLoading from '@/components/AppLoading';
import { getBasename } from '@/consts';
import AppBoundary from '@/containers/AppBoundary';
import ProtectedRoutes from '@/containers/ProtectedRoutes';
import { AppThemeContext } from '@/contexts/AppThemeProvider';
import Toast from '@/ds-components/Toast';
import useSwrOptions from '@/hooks/use-swr-options';
import useValidateTenantAccess from '@/hooks/use-validate-tenant-access';
import NotFound from '@/pages/NotFound';
import * as styles from './App.module.scss';
import { gtagAwTrackingId, gtagSignUpConversionId, logtoProductionHostname } from './constants';
import AppContent from './containers/AppContent';
import useUserOnboardingData from './hooks/use-user-onboarding-data';
import * as styles from './index.module.scss';
import About from './pages/About';
import Congrats from './pages/Congrats';
import SignInExperience from './pages/SignInExperience';
@ -32,9 +32,10 @@ const welcomePathname = getOnboardingPage(OnboardingPage.Welcome);
*/
const shouldReportToGtag = window.location.hostname.endsWith('.' + logtoProductionHostname);
function App() {
function Layout() {
const swrOptions = useSwrOptions();
const { setThemeOverride } = useContext(AppThemeContext);
const location = useLocation();
useEffect(() => {
setThemeOverride(Theme.Light);
@ -65,17 +66,19 @@ function App() {
}
}, []);
useValidateTenantAccess();
const {
data: { questionnaire },
isLoaded,
} = useUserOnboardingData();
if (!isLoaded) {
return <AppLoading />;
// Redirect to the welcome page if the user has not started the onboarding process.
if (!questionnaire && location.pathname !== welcomePathname) {
return <Navigate replace to={welcomePathname} />;
}
return (
<BrowserRouter basename={getBasename()}>
<>
<TrackOnce component={Component.Console} event={ConsoleEvent.Onboard} />
<div className={styles.app}>
<SWRConfig value={swrOptions}>
@ -90,33 +93,30 @@ function App() {
</Helmet>
)}
<Toast />
<Routes>
<Route index element={<Navigate replace to={welcomePathname} />} />
<Route path={`/${OnboardingRoute.Onboarding}`} element={<AppContent />}>
<Route index element={<Navigate replace to={welcomePathname} />} />
<Route path={OnboardingPage.Welcome} element={<Welcome />} />
<Route
path={OnboardingPage.AboutUser}
element={questionnaire ? <About /> : <Navigate replace to={welcomePathname} />}
/>
<Route
path={OnboardingPage.SignInExperience}
element={
questionnaire ? <SignInExperience /> : <Navigate replace to={welcomePathname} />
}
/>
<Route
path={OnboardingPage.Congrats}
element={questionnaire ? <Congrats /> : <Navigate replace to={welcomePathname} />}
/>
</Route>
<Route path="*" element={<NotFound />} />
</Routes>
<Outlet />
</AppBoundary>
</SWRConfig>
</div>
</BrowserRouter>
</>
);
}
export default App;
export function OnboardingRoutes() {
return (
<Routes>
<Route element={<ProtectedRoutes />}>
<Route element={<Layout />}>
<Route index element={<Navigate replace to={welcomePathname} />} />
<Route path={`/${OnboardingRoute.Onboarding}`} element={<AppContent />}>
<Route index element={<Navigate replace to={welcomePathname} />} />
<Route path={OnboardingPage.Welcome} element={<Welcome />} />
<Route path={OnboardingPage.AboutUser} element={<About />} />
<Route path={OnboardingPage.SignInExperience} element={<SignInExperience />} />
<Route path={OnboardingPage.Congrats} element={<Congrats />} />
</Route>
<Route path="*" element={<NotFound />} />
</Route>
</Route>
</Routes>
);
}

View file

@ -0,0 +1,46 @@
import { Component, GeneralEvent } from '@logto/app-insights/custom-event';
import { TrackOnce } from '@logto/app-insights/react';
import { Outlet, Route, Routes } from 'react-router-dom';
import { SWRConfig } from 'swr';
import AppBoundary from '@/containers/AppBoundary';
import AppContent from '@/containers/AppContent';
import ConsoleContent from '@/containers/ConsoleContent';
import ProtectedRoutes from '@/containers/ProtectedRoutes';
import Toast from '@/ds-components/Toast';
import useSwrOptions from '@/hooks/use-swr-options';
import Callback from '@/pages/Callback';
import Welcome from '@/pages/Welcome';
import HandleSocialCallback from '../Profile/containers/HandleSocialCallback';
function Layout() {
const swrOptions = useSwrOptions();
return (
<SWRConfig value={swrOptions}>
<AppBoundary>
<TrackOnce component={Component.Console} event={GeneralEvent.Visit} />
<Toast />
<Outlet />
</AppBoundary>
</SWRConfig>
);
}
export function ConsoleRoutes() {
return (
<Routes>
<Route element={<Layout />}>
<Route path="callback" element={<Callback />} />
<Route path="welcome" element={<Welcome />} />
<Route element={<ProtectedRoutes />}>
<Route path="handle-social" element={<HandleSocialCallback />} />
<Route element={<AppContent />}>
<Route path="*" element={<ConsoleContent />} />
</Route>
</Route>
</Route>
</Routes>
);
}

View file

@ -1,63 +0,0 @@
import { Component, GeneralEvent } from '@logto/app-insights/custom-event';
import { TrackOnce } from '@logto/app-insights/react';
import { useMemo } from 'react';
import {
Route,
RouterProvider,
createBrowserRouter,
createRoutesFromElements,
} from 'react-router-dom';
import { SWRConfig } from 'swr';
import { getBasename } from '@/consts';
import AppBoundary from '@/containers/AppBoundary';
import AppContent from '@/containers/AppContent';
import ConsoleContent from '@/containers/ConsoleContent';
import Toast from '@/ds-components/Toast';
import useSwrOptions from '@/hooks/use-swr-options';
import Callback from '@/pages/Callback';
import Welcome from '@/pages/Welcome';
import HandleSocialCallback from '../Profile/containers/HandleSocialCallback';
function Main() {
const swrOptions = useSwrOptions();
const router = useMemo(
() =>
createBrowserRouter(
createRoutesFromElements(
<Route path="/*">
<Route path="callback" element={<Callback />} />
<Route path="welcome" element={<Welcome />} />
<Route path="handle-social" element={<HandleSocialCallback />} />
<Route element={<AppContent />}>
<Route path="*" element={<ConsoleContent />} />
</Route>
</Route>
),
// Currently we use `window.open()` to navigate between tenants so the `useMemo` hook
// can have no dependency and the router will be created anyway. Consider integrating the
// tenant ID into the router and remove basename here if we want to use `history.pushState()`
// to navigate.
//
// Caveat: To use `history.pushState()`, we'd better to create a browser router in the upper
// level of the component tree to make the tenant ID a part of the URL. Otherwise, we need
// to handle `popstate` event to update the tenant ID when the user navigates back.
{ basename: getBasename() }
),
[]
);
return (
<SWRConfig value={swrOptions}>
<AppBoundary>
<TrackOnce component={Component.Console} event={GeneralEvent.Visit} />
<Toast />
<RouterProvider router={router} />
</AppBoundary>
</SWRConfig>
);
}
export default Main;

View file

@ -2,11 +2,12 @@ import { LogtoClientError, useLogto } from '@logto/react';
import classNames from 'classnames';
import { useEffect } from 'react';
import { useTranslation } from 'react-i18next';
import { useNavigate, useHref } from 'react-router-dom';
import { useNavigate } from 'react-router-dom';
import Logo from '@/assets/images/logo.svg';
import AppError from '@/components/AppError';
import SessionExpired from '@/components/SessionExpired';
import { getCallbackUrl } from '@/consts';
import Button from '@/ds-components/Button';
import useTheme from '@/hooks/use-theme';
@ -16,7 +17,6 @@ function Welcome() {
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' });
const navigate = useNavigate();
const { isAuthenticated, error, signIn } = useLogto();
const href = useHref('/callback');
const theme = useTheme();
useEffect(() => {
@ -28,7 +28,7 @@ function Welcome() {
if (error) {
if (error instanceof LogtoClientError) {
return <SessionExpired error={error} callbackHref={href} />;
return <SessionExpired error={error} />;
}
return <AppError errorMessage={error.message} callStack={error.stack} />;
@ -50,7 +50,7 @@ function Welcome() {
type="branding"
title="welcome.create_account"
onClick={() => {
void signIn(new URL(href, window.location.origin).toString());
void signIn(getCallbackUrl().href);
}}
/>
</div>