diff --git a/packages/connectors/connector-logto-email/package.json b/packages/connectors/connector-logto-email/package.json
index c1af55689..9f52b08ce 100644
--- a/packages/connectors/connector-logto-email/package.json
+++ b/packages/connectors/connector-logto-email/package.json
@@ -48,6 +48,6 @@
"access": "public"
},
"devDependencies": {
- "@logto/cloud": "0.2.5-2a72cc4"
+ "@logto/cloud": "0.2.5-81f06ea"
}
}
diff --git a/packages/console/package.json b/packages/console/package.json
index c0617c87c..18674ba94 100644
--- a/packages/console/package.json
+++ b/packages/console/package.json
@@ -28,7 +28,7 @@
"@fontsource/roboto-mono": "^5.0.0",
"@jest/types": "^29.5.0",
"@logto/app-insights": "workspace:^1.4.0",
- "@logto/cloud": "0.2.5-2a72cc4",
+ "@logto/cloud": "0.2.5-81f06ea",
"@logto/connector-kit": "workspace:^2.1.0",
"@logto/core-kit": "workspace:^2.3.0",
"@logto/language-kit": "workspace:^1.1.0",
diff --git a/packages/console/src/cloud/pages/Main/InvitationList/index.module.scss b/packages/console/src/cloud/pages/Main/InvitationList/index.module.scss
new file mode 100644
index 000000000..e4ecad534
--- /dev/null
+++ b/packages/console/src/cloud/pages/Main/InvitationList/index.module.scss
@@ -0,0 +1,77 @@
+@use '@/scss/underscore' as _;
+
+.container {
+ display: flex;
+ flex-direction: column;
+ height: 100%;
+ min-height: 600px;
+ background: var(--color-surface-1);
+ align-items: center;
+ justify-content: center;
+ overflow-y: auto;
+
+ .wrapper {
+ display: flex;
+ flex-direction: column;
+ width: 540px;
+ padding: _.unit(20) _.unit(17.5);
+ gap: _.unit(6);
+ background: var(--color-bg-float);
+ border-radius: 16px;
+ box-shadow: var(--shadow-1);
+ white-space: pre-wrap;
+
+ .icon {
+ width: 40px;
+ height: 40px;
+ flex-shrink: 0;
+ }
+
+ .title {
+ font: var(--font-headline-2);
+ }
+
+ .description {
+ font: var(--font-body-2);
+ color: var(--color-text-secondary);
+ }
+
+ .tenant {
+ display: flex;
+ align-items: center;
+ padding: _.unit(3) _.unit(4);
+ gap: _.unit(3);
+ border-radius: 12px;
+ border: 1px solid var(--color-divider);
+
+ .name {
+ @include _.multi-line-ellipsis(2);
+ }
+
+ .tag {
+ margin-left: _.unit(-2);
+ }
+ }
+
+ .separator {
+ display: flex;
+ align-items: center;
+ gap: _.unit(4);
+
+ span {
+ font: var(--font-body-2);
+ color: var(--color-text-secondary);
+ }
+
+ hr {
+ flex: 1;
+ border: none;
+ border-top: 1px solid var(--color-divider);
+ }
+ }
+
+ .createTenantButton {
+ width: 100%;
+ }
+ }
+}
diff --git a/packages/console/src/cloud/pages/Main/InvitationList/index.tsx b/packages/console/src/cloud/pages/Main/InvitationList/index.tsx
new file mode 100644
index 000000000..c67f3d191
--- /dev/null
+++ b/packages/console/src/cloud/pages/Main/InvitationList/index.tsx
@@ -0,0 +1,89 @@
+import { OrganizationInvitationStatus } from '@logto/schemas';
+import { useContext, useState } from 'react';
+import { useTranslation } from 'react-i18next';
+
+import Icon from '@/assets/icons/organization-preview.svg';
+import { useCloudApi } from '@/cloud/hooks/use-cloud-api';
+import { type TenantResponse, type InvitationListResponse } from '@/cloud/types/router';
+import CreateTenantModal from '@/components/CreateTenantModal';
+import TenantEnvTag from '@/components/TenantEnvTag';
+import { TenantsContext } from '@/contexts/TenantsProvider';
+import Button from '@/ds-components/Button';
+import Spacer from '@/ds-components/Spacer';
+
+import * as styles from './index.module.scss';
+
+type Props = {
+ invitations: InvitationListResponse;
+};
+
+function InvitationList({ invitations }: Props) {
+ const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' });
+ const cloudApi = useCloudApi();
+ const { prependTenant, navigateTenant } = useContext(TenantsContext);
+ const [isJoining, setIsJoining] = useState(false);
+ const [isCreateModalOpen, setIsCreateModalOpen] = useState(false);
+
+ return (
+ <>
+
+
+
{t('invitation.find_your_tenants')}
+
{t('invitation.find_tenants_description')}
+ {invitations.map(({ id, organizationId, tenantName, tenantTag }) => (
+
+
+ {tenantName}
+
+
+
+ ))}
+
+
+ {t('general.or')}
+
+
+
+
+ {
+ if (tenant) {
+ prependTenant(tenant);
+ navigateTenant(tenant.id);
+ }
+ setIsCreateModalOpen(false);
+ }}
+ />
+ >
+ );
+}
+
+export default InvitationList;
diff --git a/packages/console/src/cloud/pages/Main/index.tsx b/packages/console/src/cloud/pages/Main/index.tsx
index 51cc2de31..42947a556 100644
--- a/packages/console/src/cloud/pages/Main/index.tsx
+++ b/packages/console/src/cloud/pages/Main/index.tsx
@@ -1,9 +1,14 @@
+import { OrganizationInvitationStatus } from '@logto/schemas';
+
import AppLoading from '@/components/AppLoading';
+import { isCloud } from '@/consts/env';
import useCurrentUser from '@/hooks/use-current-user';
import useUserDefaultTenantId from '@/hooks/use-user-default-tenant-id';
+import useUserInvitations from '@/hooks/use-user-invitations';
import useUserOnboardingData from '@/onboarding/hooks/use-user-onboarding-data';
import AutoCreateTenant from './AutoCreateTenant';
+import InvitationList from './InvitationList';
import Redirect from './Redirect';
import TenantLandingPage from './TenantLandingPage';
@@ -11,6 +16,7 @@ export default function Main() {
const { isLoaded } = useCurrentUser();
const { isOnboarding } = useUserOnboardingData();
const { defaultTenantId } = useUserDefaultTenantId();
+ const { data } = useUserInvitations(OrganizationInvitationStatus.Pending);
if (!isLoaded) {
return ;
@@ -26,6 +32,11 @@ export default function Main() {
return ;
}
+ // If user has pending invitations (onboarding will be skipped), show the invitation list and allow them to quick join.
+ if (isCloud && data?.length) {
+ return ;
+ }
+
// If user has completed onboarding and still has no tenant, redirect to a special landing page.
return ;
}
diff --git a/packages/console/src/cloud/types/router.ts b/packages/console/src/cloud/types/router.ts
index 28cd8c17a..ed175b7af 100644
--- a/packages/console/src/cloud/types/router.ts
+++ b/packages/console/src/cloud/types/router.ts
@@ -19,6 +19,8 @@ export type InvoicesResponse = GuardedResponse;
+export type InvitationListResponse = GuardedResponse;
+
// The response of GET /api/tenants is TenantResponse[].
export type TenantResponse = GetArrayElementType>;
diff --git a/packages/console/src/hooks/use-user-invitations.ts b/packages/console/src/hooks/use-user-invitations.ts
new file mode 100644
index 000000000..cd6830b33
--- /dev/null
+++ b/packages/console/src/hooks/use-user-invitations.ts
@@ -0,0 +1,42 @@
+import { type OrganizationInvitationStatus } from '@logto/schemas';
+import { type Optional } from '@silverhand/essentials';
+import { useMemo } from 'react';
+import useSWR from 'swr';
+
+import { useCloudApi } from '@/cloud/hooks/use-cloud-api';
+import { type InvitationListResponse } from '@/cloud/types/router';
+
+import { type RequestError } from './use-api';
+
+/**
+ *
+ * @param status Filter invitations by status
+ * @returns The invitations with tenant info, error, and loading status.
+ */
+const useUserInvitations = (
+ status?: OrganizationInvitationStatus
+): {
+ data: Optional;
+ error: Optional;
+ isLoading: boolean;
+} => {
+ const cloudApi = useCloudApi({ hideErrorToast: true });
+ const { data, isLoading, error } = useSWR(
+ `/api/invitations}`,
+ async () => cloudApi.get('/api/invitations')
+ );
+
+ // Filter invitations by given status
+ const filteredResult = useMemo(
+ () => (status ? data?.filter((invitation) => status === invitation.status) : data),
+ [data, status]
+ );
+
+ return {
+ data: filteredResult,
+ error,
+ isLoading,
+ };
+};
+
+export default useUserInvitations;
diff --git a/packages/console/src/pages/AcceptInvitation/SwitchAccount/index.module.scss b/packages/console/src/pages/AcceptInvitation/SwitchAccount/index.module.scss
index 351fbdbdd..3344a666d 100644
--- a/packages/console/src/pages/AcceptInvitation/SwitchAccount/index.module.scss
+++ b/packages/console/src/pages/AcceptInvitation/SwitchAccount/index.module.scss
@@ -3,12 +3,12 @@
.container {
display: flex;
flex-direction: column;
- width: 100vw;
- height: 100vh;
+ height: 100%;
+ min-height: 600px;
background: var(--color-surface-1);
align-items: center;
justify-content: center;
- overflow: hidden;
+ overflow-y: auto;
.wrapper {
display: flex;
diff --git a/packages/console/src/pages/AcceptInvitation/index.tsx b/packages/console/src/pages/AcceptInvitation/index.tsx
index 4bd66ea24..c4a73548b 100644
--- a/packages/console/src/pages/AcceptInvitation/index.tsx
+++ b/packages/console/src/pages/AcceptInvitation/index.tsx
@@ -35,7 +35,7 @@ function AcceptInvitation() {
return;
}
(async () => {
- const { id, tenantId } = invitation;
+ const { id, organizationId } = invitation;
// Accept the invitation and redirect to the tenant page.
await cloudApi.patch(`/api/invitations/:invitationId/status`, {
@@ -43,7 +43,7 @@ function AcceptInvitation() {
body: { status: OrganizationInvitationStatus.Accepted },
});
- navigateTenant(tenantId);
+ navigateTenant(organizationId.slice(2));
})();
}, [cloudApi, error, invitation, navigateTenant, t]);
diff --git a/packages/core/package.json b/packages/core/package.json
index 012a6c3e9..efd7edf48 100644
--- a/packages/core/package.json
+++ b/packages/core/package.json
@@ -91,7 +91,7 @@
"zod": "^3.22.4"
},
"devDependencies": {
- "@logto/cloud": "0.2.5-2a72cc4",
+ "@logto/cloud": "0.2.5-81f06ea",
"@silverhand/eslint-config": "5.0.0",
"@silverhand/ts-config": "5.0.0",
"@types/debug": "^4.1.7",
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 371ff877b..0ab70c2cf 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -1235,8 +1235,8 @@ importers:
version: 3.22.4
devDependencies:
'@logto/cloud':
- specifier: 0.2.5-2a72cc4
- version: 0.2.5-2a72cc4(zod@3.22.4)
+ specifier: 0.2.5-81f06ea
+ version: 0.2.5-81f06ea(zod@3.22.4)
'@rollup/plugin-commonjs':
specifier: ^25.0.0
version: 25.0.7(rollup@4.12.0)
@@ -2715,8 +2715,8 @@ importers:
specifier: workspace:^1.4.0
version: link:../app-insights
'@logto/cloud':
- specifier: 0.2.5-2a72cc4
- version: 0.2.5-2a72cc4(zod@3.22.4)
+ specifier: 0.2.5-81f06ea
+ version: 0.2.5-81f06ea(zod@3.22.4)
'@logto/connector-kit':
specifier: workspace:^2.1.0
version: link:../toolkit/connector-kit
@@ -3202,8 +3202,8 @@ importers:
version: 3.22.4
devDependencies:
'@logto/cloud':
- specifier: 0.2.5-2a72cc4
- version: 0.2.5-2a72cc4(zod@3.22.4)
+ specifier: 0.2.5-81f06ea
+ version: 0.2.5-81f06ea(zod@3.22.4)
'@silverhand/eslint-config':
specifier: 5.0.0
version: 5.0.0(eslint@8.44.0)(prettier@3.0.0)(typescript@5.3.3)
@@ -7711,8 +7711,8 @@ packages:
jose: 5.2.2
dev: true
- /@logto/cloud@0.2.5-2a72cc4(zod@3.22.4):
- resolution: {integrity: sha512-7+2VAQBzTix/uaz5XzF/IVtU6AHLIwXLR05/sEO6dgJnhHQhcmk+PHIw4Gw9wBMVoFa3k6DiF0NQJmay80LIUA==}
+ /@logto/cloud@0.2.5-81f06ea(zod@3.22.4):
+ resolution: {integrity: sha512-7u2VY8qlRoaheWDEbHdoFmQP9MbloKuuCwbz1jk+Wrn2EE1v+tgixVK/MiyFaAN5mLAVLAlCVQ00JIabw+g6YA==}
engines: {node: ^20.9.0}
dependencies:
'@silverhand/essentials': 2.9.0