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();