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) => (
- +