diff --git a/packages/console/src/cloud/pages/Main/Redirect.tsx b/packages/console/src/cloud/pages/Main/Redirect.tsx index 863f75429..2e4f1e64b 100644 --- a/packages/console/src/cloud/pages/Main/Redirect.tsx +++ b/packages/console/src/cloud/pages/Main/Redirect.tsx @@ -1,41 +1,17 @@ -import { useLogto } from '@logto/react'; -import { trySafe } from '@silverhand/essentials'; import { useContext, useEffect } from 'react'; -import { useHref } from 'react-router-dom'; import AppLoading from '@/components/AppLoading'; import { TenantsContext } from '@/contexts/TenantsProvider'; function Redirect() { - const { getAccessToken, signIn } = useLogto(); - const { navigateTenant, tenants, currentTenantId } = useContext(TenantsContext); - - const tenant = tenants.find(({ id }) => id === currentTenantId); - const href = useHref(currentTenantId + '/callback'); + const { navigateTenant, tenants, currentTenant } = useContext(TenantsContext); useEffect(() => { - const validate = async (indicator: string) => { - // 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))) { - navigateTenant(currentTenantId); - } else { - void signIn(new URL(href, window.location.origin).toString()); - } - }; - - if (tenant) { - void validate(tenant.indicator); - } - }, [currentTenantId, getAccessToken, href, navigateTenant, signIn, tenant]); - - useEffect(() => { - if (!tenant) { + if (!currentTenant) { /** Fallback to another available tenant instead of showing `Forbidden`. */ navigateTenant(tenants[0]?.id ?? ''); } - }, [navigateTenant, tenant, tenants]); + }, [navigateTenant, currentTenant, tenants]); return ; } diff --git a/packages/console/src/consts/tenants.ts b/packages/console/src/consts/tenants.ts index 55d4ee73a..a9319eb91 100644 --- a/packages/console/src/consts/tenants.ts +++ b/packages/console/src/consts/tenants.ts @@ -1,4 +1,5 @@ import { defaultTenantId, ossConsolePath } from '@logto/schemas'; +import { conditionalArray } from '@silverhand/essentials'; import { CloudRoute } from '@/cloud/types'; @@ -36,6 +37,9 @@ export const getUserTenantId = () => { export const getBasename = () => (isCloud ? '/' + getUserTenantId() : ossConsolePath); +export const getCallbackUrl = (tenantId?: string) => + new URL('/' + conditionalArray(tenantId, 'callback').join('/'), window.location.origin); + export const getSignOutRedirectPathname = () => (isCloud ? '/' : ossConsolePath); export const maxFreeTenantNumbers = 3; diff --git a/packages/console/src/containers/AppContent/components/Topbar/TenantSelector/index.tsx b/packages/console/src/containers/AppContent/components/Topbar/TenantSelector/index.tsx index cc14843db..256c8654e 100644 --- a/packages/console/src/containers/AppContent/components/Topbar/TenantSelector/index.tsx +++ b/packages/console/src/containers/AppContent/components/Topbar/TenantSelector/index.tsx @@ -25,6 +25,7 @@ export default function TenantSelector() { appendTenant, currentTenant: currentTenantInfo, currentTenantId, + navigateTenant, } = useContext(TenantsContext); const isCreateButtonDisabled = useMemo( @@ -75,7 +76,8 @@ export default function TenantSelector() { key={id} className={styles.dropdownItem} onClick={() => { - window.open(new URL(`/${id}`, window.location.origin).toString(), '_self'); + navigateTenant(id); + setShowDropdown(false); }} >
{name}
@@ -110,7 +112,7 @@ export default function TenantSelector() { onClose={async (tenant?: TenantInfo) => { if (tenant) { appendTenant(tenant); - window.location.assign(new URL(`/${tenant.id}`, window.location.origin).toString()); + navigateTenant(tenant.id); } setShowCreateTenantModal(false); }} diff --git a/packages/console/src/containers/TenantAppContainer/index.tsx b/packages/console/src/containers/TenantAppContainer/index.tsx index e410ee592..d5bd8f4ab 100644 --- a/packages/console/src/containers/TenantAppContainer/index.tsx +++ b/packages/console/src/containers/TenantAppContainer/index.tsx @@ -1,16 +1,38 @@ -import { useContext } from 'react'; +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 AppLoading from '@/components/AppLoading'; +import { getCallbackUrl } 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 useUserOnboardingData from '@/onboarding/hooks/use-user-onboarding-data'; import ConsoleApp from '@/pages/Main'; 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]); useTrackUserId(); diff --git a/packages/console/src/contexts/TenantsProvider.tsx b/packages/console/src/contexts/TenantsProvider.tsx index 4838aadae..bbdd09748 100644 --- a/packages/console/src/contexts/TenantsProvider.tsx +++ b/packages/console/src/contexts/TenantsProvider.tsx @@ -76,19 +76,10 @@ function TenantsProvider({ children }: Props) { const [isInitComplete, setIsInitComplete] = useState(!isCloud); const [currentTenantId, setCurrentTenantId] = useState(getUserTenantId()); - const navigateTenant = useCallback((tenantId: string, options?: NavigateOptions) => { - const params = [ - options?.state ?? {}, - '', - new URL(`/${tenantId}`, window.location.origin).toString(), - ] satisfies Parameters; - - if (options?.replace) { - window.history.replaceState(...params); - return; - } - - window.history.pushState(...params); + 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); }, []); diff --git a/packages/console/src/pages/Main/index.tsx b/packages/console/src/pages/Main/index.tsx index 8a629d21c..0bf2eae89 100644 --- a/packages/console/src/pages/Main/index.tsx +++ b/packages/console/src/pages/Main/index.tsx @@ -22,19 +22,28 @@ import HandleSocialCallback from '../Profile/containers/HandleSocialCallback'; function Main() { const swrOptions = useSwrOptions(); + const router = useMemo( () => createBrowserRouter( createRoutesFromElements( - <> + } /> } /> } /> }> - } /> + } /> - + ), + // 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() } ), [] diff --git a/packages/console/src/pages/TenantSettings/TenantBasicSettings/index.tsx b/packages/console/src/pages/TenantSettings/TenantBasicSettings/index.tsx index d36fc851e..3febf319f 100644 --- a/packages/console/src/pages/TenantSettings/TenantBasicSettings/index.tsx +++ b/packages/console/src/pages/TenantSettings/TenantBasicSettings/index.tsx @@ -101,20 +101,6 @@ function TenantBasicSettings() { } }; - useEffect(() => { - /** - * Redirect to the first tenant if the current tenant is deleted; - * Redirect to Cloud console landing page if there is no tenant. - */ - if (!tenants.some(({ id }) => id === currentTenantId)) { - window.location.assign( - tenants[0]?.id - ? new URL(`/${tenants[0]?.id}`, window.location.origin).toString() - : new URL(window.location.origin).toString() - ); - } - }, [currentTenantId, tenants]); - if (error) { return ; } diff --git a/packages/integration-tests/src/tests/ui-cloud/smoke.test.ts b/packages/integration-tests/src/tests/ui-cloud/smoke.test.ts index d329d3228..2b68f740a 100644 --- a/packages/integration-tests/src/tests/ui-cloud/smoke.test.ts +++ b/packages/integration-tests/src/tests/ui-cloud/smoke.test.ts @@ -75,7 +75,7 @@ describe('smoke testing for cloud', () => { await expect(page).toClick('div[role=button][class$=item]'); // Click the next button - await expect(page).toClick('div[class$=actions] button:first-child'); + await expect(page).toClick('div[class$=actions] button', { text: 'Next' }); // Wait for the next page to load await expect(page).toMatchElement('div[class$=config] div[class$=title]', { @@ -127,7 +127,7 @@ describe('smoke testing for cloud', () => { await page.waitForTimeout(500); const createTenantButton = await page.waitForSelector( - 'div[class$=ReactModalPortal] div[class$=dropdownContainer] > div[class$=dropdown] > div[class$=createTenantButton][role=button]:has(div)' + 'div[class$=ReactModalPortal] div[class$=dropdownContainer] > div[class$=dropdown] > button[class$=createTenantButton]:has(div)' ); await expect(createTenantButton).toMatchElement('div', { text: 'Create tenant' }); await createTenantButton.click();