diff --git a/packages/console/src/cloud/pages/SocialDemoCallback/index.tsx b/packages/console/src/cloud/pages/SocialDemoCallback/index.tsx
index b83cd0c46..eb8d11eda 100644
--- a/packages/console/src/cloud/pages/SocialDemoCallback/index.tsx
+++ b/packages/console/src/cloud/pages/SocialDemoCallback/index.tsx
@@ -26,8 +26,8 @@ function SocialDemoCallback() {
{theme === Theme.Light ? : }
- {t('cloud.socialCallback.title')}
- {t('cloud.socialCallback.description')}
+ {t('cloud.social_callback.title')}
+ {t('cloud.social_callback.description')}
);
diff --git a/packages/console/src/components/ActionBar/index.tsx b/packages/console/src/components/ActionBar/index.tsx
index 403f3a981..19f94ee09 100644
--- a/packages/console/src/components/ActionBar/index.tsx
+++ b/packages/console/src/components/ActionBar/index.tsx
@@ -4,17 +4,23 @@ import ProgressBar from '../ProgressBar';
import styles from './index.module.scss';
-type Props = {
- readonly step: number;
- readonly totalSteps: number;
- readonly children: ReactNode;
-};
+type Props =
+ | {
+ readonly step: number;
+ readonly totalSteps: number;
+ readonly children: ReactNode;
+ }
+ | {
+ readonly children: ReactNode;
+ };
-function ActionBar({ step, totalSteps, children }: Props) {
+function ActionBar(props: Props) {
return (
-
-
{children}
+ {'step' in props && 'totalSteps' in props && (
+
+ )}
+
{props.children}
);
}
diff --git a/packages/console/src/components/CreateTenantModal/index.tsx b/packages/console/src/components/CreateTenantModal/index.tsx
index db7b7fedf..c60fb95df 100644
--- a/packages/console/src/components/CreateTenantModal/index.tsx
+++ b/packages/console/src/components/CreateTenantModal/index.tsx
@@ -10,6 +10,7 @@ import CreateTenantHeaderIcon from '@/assets/icons/create-tenant-header.svg?reac
import { useCloudApi } from '@/cloud/hooks/use-cloud-api';
import { type TenantResponse } from '@/cloud/types/router';
import Region, { RegionName } from '@/components/Region';
+import { availableRegions } from '@/consts';
import Button from '@/ds-components/Button';
import DangerousRaw from '@/ds-components/DangerousRaw';
import FormField from '@/ds-components/FormField';
@@ -124,8 +125,7 @@ function CreateTenantModal({ isOpen, onClose }: Props) {
rules={{ required: true }}
render={({ field: { onChange, value, name } }) => (
- {/* Manually maintaining the list of regions to avoid unexpected changes. We may consider using an API in the future. */}
- {[RegionName.EU, RegionName.US, RegionName.AU].map((region) => (
+ {availableRegions.map((region) => (
{
export const adminTenantEndpoint = getAdminTenantEndpoint();
export const mainTitle = isCloud ? 'Logto Cloud' : 'Logto Console';
+
+// Manually maintaining the list of regions to avoid unexpected changes. We may consider using an API in the future.
+export const availableRegions = Object.freeze([
+ RegionName.EU,
+ RegionName.US,
+ RegionName.AU,
+] as const);
diff --git a/packages/console/src/contexts/AppDataProvider.tsx b/packages/console/src/contexts/AppDataProvider.tsx
index f52562402..a96657a29 100644
--- a/packages/console/src/contexts/AppDataProvider.tsx
+++ b/packages/console/src/contexts/AppDataProvider.tsx
@@ -22,7 +22,7 @@ type AppData = {
export const AppDataContext = createContext({});
-export const useTenantEndpoint = (tenantId: string) => {
+const useTenantEndpoint = (tenantId: string) => {
return useSWRImmutable(`api/.well-known/endpoints/${tenantId}`, async (pathname) => {
const { user } = await ky.get(new URL(pathname, adminTenantEndpoint)).json<{ user: string }>();
return new URL(user);
diff --git a/packages/console/src/onboarding/components/CardSelector/CardSelector.tsx b/packages/console/src/onboarding/components/CardSelector/CardSelector.tsx
deleted file mode 100644
index 05a455eaf..000000000
--- a/packages/console/src/onboarding/components/CardSelector/CardSelector.tsx
+++ /dev/null
@@ -1,29 +0,0 @@
-import RadioGroup, { Radio } from '@/ds-components/RadioGroup';
-
-import type { CardSelectorOption } from './types';
-
-type Props = {
- readonly name: string;
- readonly value: string;
- readonly options: CardSelectorOption[];
- readonly onChange: (value: string) => void;
- readonly optionClassName?: string;
-};
-
-function CardSelector({ name, value, options, onChange, optionClassName }: Props) {
- return (
-
- {options.map(({ value: optionValue, title, icon }) => (
-
- ))}
-
- );
-}
-
-export default CardSelector;
diff --git a/packages/console/src/onboarding/components/CardSelector/MultiCardSelector/CardItem.module.scss b/packages/console/src/onboarding/components/CardSelector/MultiCardSelector/CardItem.module.scss
deleted file mode 100644
index 732c58af1..000000000
--- a/packages/console/src/onboarding/components/CardSelector/MultiCardSelector/CardItem.module.scss
+++ /dev/null
@@ -1,65 +0,0 @@
-@use '@/scss/underscore' as _;
-
-.item {
- border: 1px solid var(--color-border);
- border-radius: 12px;
- min-height: 80px;
- padding: _.unit(5);
- font: var(--font-label-2);
- user-select: none;
- background-color: var(--color-layer-1);
- color: var(--color-text);
- display: flex;
- align-items: center;
-
- .icon {
- color: var(--color-text-secondary);
- margin-inline-end: _.unit(4);
- vertical-align: middle;
-
- > svg {
- display: block;
- }
- }
-
- .content {
- .tag {
- font: var(--font-body-3);
- color: var(--color-text-secondary);
- }
-
- .trailingTag {
- margin-inline-start: _.unit(1);
- }
- }
-
- &.disabled {
- border-color: var(--color-layer-2);
- background-color: var(--color-layer-2);
-
- &:hover {
- cursor: not-allowed;
- }
- }
-
- &:not(.disabled).selected {
- border-color: var(--color-primary);
- background-color: var(--color-hover-variant);
- color: var(--color-primary);
-
- .icon {
- color: var(--color-primary);
- }
- }
-
- &:not(.disabled):hover {
- cursor: pointer;
- border-color: var(--color-primary);
- color: var(--color-primary);
-
- .icon {
- color: var(--color-primary);
- }
- }
-}
-
diff --git a/packages/console/src/onboarding/components/CardSelector/MultiCardSelector/CardItem.tsx b/packages/console/src/onboarding/components/CardSelector/MultiCardSelector/CardItem.tsx
deleted file mode 100644
index 8e16db8db..000000000
--- a/packages/console/src/onboarding/components/CardSelector/MultiCardSelector/CardItem.tsx
+++ /dev/null
@@ -1,71 +0,0 @@
-import { conditional } from '@silverhand/essentials';
-import classNames from 'classnames';
-
-import DynamicT from '@/ds-components/DynamicT';
-import { Tooltip } from '@/ds-components/Tip';
-import { onKeyDownHandler } from '@/utils/a11y';
-
-import type { MultiCardSelectorOption } from '../types';
-
-import styles from './CardItem.module.scss';
-
-type Props = {
- readonly option: MultiCardSelectorOption;
- readonly isSelected: boolean;
- readonly onClick: (value: string) => void;
- readonly className?: string;
-};
-
-function CardItem({
- option: { icon, title, value, tag, trailingTag, isDisabled, disabledTip },
- isSelected,
- onClick,
- className,
-}: Props) {
- return (
- )}>
- {
- if (isDisabled) {
- return;
- }
- onClick(value);
- }}
- onKeyDown={onKeyDownHandler(() => {
- if (isDisabled) {
- return;
- }
- onClick(value);
- })}
- >
- {icon &&
{icon}}
-
-
- {typeof title === 'string' ? : title}
- {trailingTag && (
-
-
-
- )}
-
- {tag && (
-
-
-
- )}
-
-
-
- );
-}
-
-export default CardItem;
diff --git a/packages/console/src/onboarding/components/CardSelector/MultiCardSelector/index.module.scss b/packages/console/src/onboarding/components/CardSelector/MultiCardSelector/index.module.scss
deleted file mode 100644
index 8621693ac..000000000
--- a/packages/console/src/onboarding/components/CardSelector/MultiCardSelector/index.module.scss
+++ /dev/null
@@ -1,7 +0,0 @@
-@use '@/scss/underscore' as _;
-
-.selector {
- display: grid;
- grid-template-columns: repeat(3, 1fr);
- gap: _.unit(4);
-}
diff --git a/packages/console/src/onboarding/components/CardSelector/MultiCardSelector/index.tsx b/packages/console/src/onboarding/components/CardSelector/MultiCardSelector/index.tsx
deleted file mode 100644
index 7ed883c2a..000000000
--- a/packages/console/src/onboarding/components/CardSelector/MultiCardSelector/index.tsx
+++ /dev/null
@@ -1,52 +0,0 @@
-import classNames from 'classnames';
-
-import type { MultiCardSelectorOption } from '../types';
-
-import CardItem from './CardItem';
-import styles from './index.module.scss';
-
-type Props = {
- readonly options: MultiCardSelectorOption[];
- readonly value: string[];
- readonly onChange: (value: string[]) => void;
- readonly isNotAllowEmpty?: boolean;
- readonly className?: string;
- readonly optionClassName?: string;
-};
-
-function MultiCardSelector({
- options,
- value: selectedValues,
- onChange,
- isNotAllowEmpty = false,
- className,
- optionClassName,
-}: Props) {
- const onToggle = (value: string) => {
- if (selectedValues.includes(value) && selectedValues.length === 1 && isNotAllowEmpty) {
- return;
- }
-
- onChange(
- selectedValues.includes(value)
- ? selectedValues.filter((selected) => selected !== value)
- : [...selectedValues, value]
- );
- };
-
- return (
-
- {options.map((option) => (
-
- ))}
-
- );
-}
-
-export default MultiCardSelector;
diff --git a/packages/console/src/onboarding/components/CardSelector/index.tsx b/packages/console/src/onboarding/components/CardSelector/index.tsx
deleted file mode 100644
index 9ee8012a5..000000000
--- a/packages/console/src/onboarding/components/CardSelector/index.tsx
+++ /dev/null
@@ -1,3 +0,0 @@
-export type { CardSelectorOption, MultiCardSelectorOption } from './types';
-export { default as CardSelector } from './CardSelector';
-export { default as MultiCardSelector } from './MultiCardSelector';
diff --git a/packages/console/src/onboarding/components/CardSelector/types.ts b/packages/console/src/onboarding/components/CardSelector/types.ts
deleted file mode 100644
index 2e3791117..000000000
--- a/packages/console/src/onboarding/components/CardSelector/types.ts
+++ /dev/null
@@ -1,17 +0,0 @@
-import type { AdminConsoleKey } from '@logto/phrases';
-import type { ReactElement, ReactNode } from 'react';
-
-import type DangerousRaw from '@/ds-components/DangerousRaw';
-
-export type CardSelectorOption = {
- icon?: ReactNode;
- title: AdminConsoleKey | ReactElement;
- value: string;
-};
-
-export type MultiCardSelectorOption = CardSelectorOption & {
- tag?: AdminConsoleKey;
- trailingTag?: AdminConsoleKey;
- isDisabled?: boolean;
- disabledTip?: AdminConsoleKey;
-};
diff --git a/packages/console/src/onboarding/components/DemoConnectorNotice/index.tsx b/packages/console/src/onboarding/components/DemoConnectorNotice/index.tsx
index 4ea1aa2e6..052366c65 100644
--- a/packages/console/src/onboarding/components/DemoConnectorNotice/index.tsx
+++ b/packages/console/src/onboarding/components/DemoConnectorNotice/index.tsx
@@ -9,7 +9,7 @@ function DemoConnectorNotice() {
return (
- {t('cloud.sie.connectors.notice')}
+ {t('cloud.social_callback.notice')}
);
}
diff --git a/packages/console/src/onboarding/hooks/use-tenant-api.ts b/packages/console/src/onboarding/hooks/use-tenant-api.ts
deleted file mode 100644
index 33a377464..000000000
--- a/packages/console/src/onboarding/hooks/use-tenant-api.ts
+++ /dev/null
@@ -1,36 +0,0 @@
-import { buildOrganizationUrn } from '@logto/core-kit';
-import { getTenantOrganizationId } from '@logto/schemas';
-import { appendPath } from '@silverhand/essentials';
-import { useMemo } from 'react';
-import { useParams } from 'react-router-dom';
-
-import { type StaticApiProps, useStaticApi } from '@/hooks/use-api';
-
-/**
- * A hook to get a Ky instance with the current tenant's Management API prefix URL. Note this hook
- * can only be used in a route with a `:tenantId` param.
- */
-const useTenantApi = (props: Omit = {}) => {
- const { tenantId: currentTenantId } = useParams();
-
- if (!currentTenantId) {
- throw new Error(
- 'No tenant ID param found in the current route. This hook should be used in a route with a tenant ID param.'
- );
- }
-
- const config = useMemo(
- () => ({
- prefixUrl: appendPath(new URL(window.location.origin), 'm', currentTenantId),
- resourceIndicator: buildOrganizationUrn(getTenantOrganizationId(currentTenantId)),
- }),
- [currentTenantId]
- );
-
- return useStaticApi({
- ...props,
- ...config,
- });
-};
-
-export default useTenantApi;
diff --git a/packages/console/src/onboarding/hooks/use-tenant-swr-options.ts b/packages/console/src/onboarding/hooks/use-tenant-swr-options.ts
deleted file mode 100644
index c689d718b..000000000
--- a/packages/console/src/onboarding/hooks/use-tenant-swr-options.ts
+++ /dev/null
@@ -1,28 +0,0 @@
-import type React from 'react';
-import { useMemo } from 'react';
-import type { SWRConfig } from 'swr';
-
-import useSwrFetcher from '@/hooks/use-swr-fetcher';
-import { shouldRetryOnError } from '@/utils/request';
-
-import useTenantApi from './use-tenant-api';
-
-/**
- * A hook to get the SWR options for the current tenant by reading the `:tenantId` param from the
- * route.
- */
-const useTenantSwrOptions = (): Partial['value']> => {
- const api = useTenantApi();
- const fetcher = useSwrFetcher(api);
-
- const config = useMemo(
- () => ({
- fetcher,
- shouldRetryOnError: shouldRetryOnError({ ignore: [401, 403] }),
- }),
- [fetcher]
- );
- return config;
-};
-
-export default useTenantSwrOptions;
diff --git a/packages/console/src/onboarding/hooks/use-tenant-user-asset-service.ts b/packages/console/src/onboarding/hooks/use-tenant-user-asset-service.ts
deleted file mode 100644
index a088d6439..000000000
--- a/packages/console/src/onboarding/hooks/use-tenant-user-asset-service.ts
+++ /dev/null
@@ -1,27 +0,0 @@
-import { type UserAssetsServiceStatus } from '@logto/schemas';
-import useSWRImmutable from 'swr/immutable';
-
-import { type RequestError } from '@/hooks/use-api';
-import useSwrFetcher from '@/hooks/use-swr-fetcher';
-
-import useTenantApi from './use-tenant-api';
-
-/**
- * A hook to check if the current tenant's user assets service is ready. The tenant ID is read from
- * `:tenantId` param in the route.
- */
-const useTenantUserAssetsService = () => {
- const api = useTenantApi();
- const fetcher = useSwrFetcher(api);
- const { data, error } = useSWRImmutable(
- 'api/user-assets/service-status',
- fetcher
- );
-
- return {
- isReady: data?.status === 'ready',
- isLoading: !error && !data,
- };
-};
-
-export default useTenantUserAssetsService;
diff --git a/packages/console/src/onboarding/index.tsx b/packages/console/src/onboarding/index.tsx
index dd03850f0..112833cde 100644
--- a/packages/console/src/onboarding/index.tsx
+++ b/packages/console/src/onboarding/index.tsx
@@ -1,6 +1,6 @@
import { Theme } from '@logto/schemas';
import { useContext, useEffect } from 'react';
-import { Navigate, type RouteObject, useMatch, useRoutes } from 'react-router-dom';
+import { Navigate, type RouteObject, useRoutes } from 'react-router-dom';
import AppLoading from '@/components/AppLoading';
import AppBoundary from '@/containers/AppBoundary';
@@ -11,35 +11,21 @@ import Topbar from './components/Topbar';
import useUserOnboardingData from './hooks/use-user-onboarding-data';
import styles from './index.module.scss';
import CreateTenant from './pages/CreateTenant';
-import SignInExperience from './pages/SignInExperience';
-import Welcome from './pages/Welcome';
import { OnboardingPage } from './types';
-import { getOnboardingPage } from './utils';
-
-const welcomePathname = getOnboardingPage(OnboardingPage.Welcome);
const routeObjects: RouteObject[] = [
{
index: true,
- element: ,
- },
- {
- path: OnboardingPage.Welcome,
- element: ,
+ element: ,
},
{
path: OnboardingPage.CreateTenant,
element: ,
},
- {
- path: `:tenantId/${OnboardingPage.SignInExperience}`,
- element: ,
- },
];
export function OnboardingApp() {
const { setThemeOverride } = useContext(AppThemeContext);
- const matched = useMatch(welcomePathname);
const routes = useRoutes(routeObjects);
usePlausiblePageview(routeObjects, 'onboarding');
@@ -54,7 +40,7 @@ export function OnboardingApp() {
const {
isLoading,
- data: { questionnaire, isOnboardingDone },
+ data: { isOnboardingDone },
} = useUserOnboardingData();
if (isLoading) {
@@ -65,11 +51,6 @@ export function OnboardingApp() {
return ;
}
- // Redirect to the welcome page if the user has not started the onboarding process.
- if (!questionnaire && !matched) {
- return ;
- }
-
return (
diff --git a/packages/console/src/onboarding/pages/CreateTenant/index.tsx b/packages/console/src/onboarding/pages/CreateTenant/index.tsx
index 9d591ccab..3a6b6c045 100644
--- a/packages/console/src/onboarding/pages/CreateTenant/index.tsx
+++ b/packages/console/src/onboarding/pages/CreateTenant/index.tsx
@@ -1,7 +1,6 @@
import { emailRegEx } from '@logto/core-kit';
import { useLogto } from '@logto/react';
import { TenantRole, Theme } from '@logto/schemas';
-import { joinPath } from '@silverhand/essentials';
import { useCallback, useContext } from 'react';
import { Controller, FormProvider, useForm } from 'react-hook-form';
import { toast } from 'react-hot-toast';
@@ -11,9 +10,11 @@ import CreateTenantHeaderIconDark from '@/assets/icons/create-tenant-header-dark
import CreateTenantHeaderIcon from '@/assets/icons/create-tenant-header.svg?react';
import { createTenantApi, useCloudApi } from '@/cloud/hooks/use-cloud-api';
import ActionBar from '@/components/ActionBar';
+import { GtagConversionId, reportConversion } from '@/components/Conversion/utils';
import { type CreateTenantData } from '@/components/CreateTenantModal/types';
import PageMeta from '@/components/PageMeta';
import Region, { RegionName } from '@/components/Region';
+import { availableRegions } from '@/consts';
import { TenantsContext } from '@/contexts/TenantsProvider';
import Button from '@/ds-components/Button';
import DangerousRaw from '@/ds-components/DangerousRaw';
@@ -21,10 +22,10 @@ import FormField from '@/ds-components/FormField';
import OverlayScrollbar from '@/ds-components/OverlayScrollbar';
import RadioGroup, { Radio } from '@/ds-components/RadioGroup';
import TextInput from '@/ds-components/TextInput';
-import useTenantPathname from '@/hooks/use-tenant-pathname';
+import useCurrentUser from '@/hooks/use-current-user';
import useTheme from '@/hooks/use-theme';
+import useUserOnboardingData from '@/onboarding/hooks/use-user-onboarding-data';
import pageLayout from '@/onboarding/scss/layout.module.scss';
-import { OnboardingPage, OnboardingRoute } from '@/onboarding/types';
import InviteEmailsInput from '@/pages/TenantSettings/TenantMembers/InviteEmailsInput';
import { type InviteeEmailItem } from '@/pages/TenantSettings/TenantMembers/types';
import { trySubmitSafe } from '@/utils/form';
@@ -33,7 +34,7 @@ type CreateTenantForm = Omit & { collaboratorEmails: In
function CreateTenant() {
const methods = useForm({
- defaultValues: { regionName: RegionName.EU, collaboratorEmails: [] },
+ defaultValues: { name: 'My project', regionName: RegionName.EU, collaboratorEmails: [] },
});
const {
control,
@@ -41,10 +42,10 @@ function CreateTenant() {
formState: { errors, isSubmitting },
register,
} = methods;
- const { navigate } = useTenantPathname();
const { prependTenant } = useContext(TenantsContext);
const theme = useTheme();
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' });
+ const { update } = useUserOnboardingData();
const parseEmailOptions = useCallback(
(values: InviteeEmailItem[]) => {
const validEmails = values.filter(({ value }) => emailRegEx.test(value));
@@ -62,9 +63,15 @@ function CreateTenant() {
const { isAuthenticated, getOrganizationToken } = useLogto();
const cloudApi = useCloudApi();
+ const { user } = useCurrentUser();
const onCreateClick = handleSubmit(
trySubmitSafe(async ({ name, regionName, collaboratorEmails }: CreateTenantForm) => {
+ reportConversion({
+ gtagId: GtagConversionId.SignUp,
+ redditType: 'SignUp',
+ transactionId: user?.id,
+ });
const newTenant = await cloudApi.post('/api/tenants', {
body: { name: name || 'My project', regionName },
});
@@ -93,7 +100,7 @@ function CreateTenant() {
toast.error(t('tenants.create_modal.invitation_failed', { duration: 5 }));
}
}
- navigate(joinPath(OnboardingRoute.Onboarding, newTenant.id, OnboardingPage.SignInExperience));
+ await update({ isOnboardingDone: true });
})
);
@@ -126,8 +133,7 @@ function CreateTenant() {
rules={{ required: true }}
render={({ field: { onChange, value, name } }) => (
- {/* Manually maintaining the list of regions to avoid unexpected changes. We may consider using an API in the future. */}
- {[RegionName.EU, RegionName.US].map((region) => (
+ {availableRegions.map((region) => (
-
+