mirror of
https://github.com/logto-io/logto.git
synced 2025-01-20 21:32:31 -05:00
refactor(console): fix onboarding issues
This commit is contained in:
parent
d5f67ecc77
commit
b755e95a75
4 changed files with 158 additions and 119 deletions
|
@ -23,13 +23,25 @@ type Props = {
|
|||
readonly mode: Theme;
|
||||
readonly language?: LanguageTag;
|
||||
readonly signInExperience?: SignInExperience;
|
||||
/**
|
||||
* The Logto endpoint to use for the preview. If not provided, the current tenant endpoint from
|
||||
* the `AppDataContext` will be used.
|
||||
*/
|
||||
readonly endpoint?: URL;
|
||||
};
|
||||
|
||||
function SignInExperiencePreview({ platform, mode, language = 'en', signInExperience }: Props) {
|
||||
function SignInExperiencePreview({
|
||||
platform,
|
||||
mode,
|
||||
language = 'en',
|
||||
signInExperience,
|
||||
endpoint: endpointInput,
|
||||
}: Props) {
|
||||
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' });
|
||||
|
||||
const { customPhrases } = useUiLanguages();
|
||||
const { tenantEndpoint } = useContext(AppDataContext);
|
||||
const endpoint = endpointInput ?? tenantEndpoint;
|
||||
const previewRef = useRef<HTMLIFrameElement>(null);
|
||||
const { data: allConnectors } = useSWR<ConnectorResponse[], RequestError>('api/connectors');
|
||||
const [iframeLoaded, setIframeLoaded] = useState(false);
|
||||
|
@ -76,9 +88,9 @@ function SignInExperiencePreview({ platform, mode, language = 'en', signInExperi
|
|||
|
||||
previewRef.current?.contentWindow?.postMessage(
|
||||
{ sender: 'ac_preview', config: configForUiPage },
|
||||
tenantEndpoint?.origin ?? ''
|
||||
endpoint?.origin ?? ''
|
||||
);
|
||||
}, [tenantEndpoint?.origin, configForUiPage, customPhrases]);
|
||||
}, [endpoint?.origin, configForUiPage, customPhrases]);
|
||||
|
||||
const iframeOnLoadEventHandler = useCallback(() => {
|
||||
setIframeLoaded(true);
|
||||
|
@ -102,7 +114,7 @@ function SignInExperiencePreview({ platform, mode, language = 'en', signInExperi
|
|||
postPreviewMessage();
|
||||
}, [iframeLoaded, postPreviewMessage]);
|
||||
|
||||
if (!tenantEndpoint) {
|
||||
if (!endpoint) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
@ -131,7 +143,7 @@ function SignInExperiencePreview({ platform, mode, language = 'en', signInExperi
|
|||
ref={previewRef}
|
||||
// Allow all sandbox rules
|
||||
sandbox={undefined}
|
||||
src={new URL('/sign-in?preview=true', tenantEndpoint).toString()}
|
||||
src={new URL('/sign-in?preview=true', endpoint).toString()}
|
||||
tabIndex={-1}
|
||||
title={t('sign_in_exp.preview.title')}
|
||||
/>
|
||||
|
|
|
@ -22,20 +22,17 @@ type AppData = {
|
|||
|
||||
export const AppDataContext = createContext<AppData>({});
|
||||
|
||||
export 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);
|
||||
});
|
||||
};
|
||||
|
||||
/** The context provider for the global app data. */
|
||||
function AppDataProvider({ children }: Props) {
|
||||
const { currentTenantId } = useContext(TenantsContext);
|
||||
|
||||
const { data: tenantEndpoint } = useSWRImmutable(
|
||||
`api/.well-known/endpoints/${currentTenantId}`,
|
||||
async (pathname) => {
|
||||
const { user } = await ky
|
||||
.get(new URL(pathname, adminTenantEndpoint))
|
||||
.json<{ user: string }>();
|
||||
return new URL(user);
|
||||
}
|
||||
);
|
||||
|
||||
const { data: tenantEndpoint } = useTenantEndpoint(currentTenantId);
|
||||
const memorizedContext = useMemo(
|
||||
() =>
|
||||
({
|
||||
|
|
|
@ -12,9 +12,14 @@ import * as styles from './index.module.scss';
|
|||
type Props = {
|
||||
readonly signInExperience?: SignInExperience;
|
||||
readonly className?: string;
|
||||
/**
|
||||
* The Logto endpoint to use for the preview. If not provided, the current tenant endpoint from
|
||||
* the `AppDataContext` will be used.
|
||||
*/
|
||||
readonly endpoint?: URL;
|
||||
};
|
||||
|
||||
function Preview({ signInExperience, className }: Props) {
|
||||
function Preview({ signInExperience, className, endpoint }: Props) {
|
||||
const [currentTab, setCurrentTab] = useState(PreviewPlatform.DesktopWeb);
|
||||
|
||||
return (
|
||||
|
@ -24,6 +29,7 @@ function Preview({ signInExperience, className }: Props) {
|
|||
platform={currentTab}
|
||||
mode={Theme.Light}
|
||||
signInExperience={signInExperience}
|
||||
endpoint={endpoint}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -4,12 +4,14 @@ import type { SignInExperience as SignInExperienceType, ConnectorResponse } from
|
|||
import { useCallback, useEffect, useMemo } from 'react';
|
||||
import { Controller, useForm } from 'react-hook-form';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import useSWR from 'swr';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import useSWR, { SWRConfig } from 'swr';
|
||||
|
||||
import Tools from '@/assets/icons/tools.svg';
|
||||
import ActionBar from '@/components/ActionBar';
|
||||
import { GtagConversionId, reportConversion } from '@/components/Conversion/utils';
|
||||
import PageMeta from '@/components/PageMeta';
|
||||
import { useTenantEndpoint } from '@/contexts/AppDataProvider';
|
||||
import Button from '@/ds-components/Button';
|
||||
import ColorPicker from '@/ds-components/ColorPicker';
|
||||
import FormField from '@/ds-components/FormField';
|
||||
|
@ -39,6 +41,19 @@ import { authenticationOptions, identifierOptions } from './options';
|
|||
import { defaultOnboardingSieFormData } from './sie-config-templates';
|
||||
import { Authentication, type OnboardingSieFormData } from './types';
|
||||
|
||||
const useCurrentTenantEndpoint = () => {
|
||||
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 { data } = useTenantEndpoint(currentTenantId);
|
||||
return data;
|
||||
};
|
||||
|
||||
function SignInExperience() {
|
||||
const swrOptions = useTenantSwrOptions();
|
||||
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' });
|
||||
|
@ -54,6 +69,7 @@ function SignInExperience() {
|
|||
const { isReady: isUserAssetsServiceReady } = useUserAssetsService();
|
||||
const { update } = useUserOnboardingData();
|
||||
const { user } = useCurrentUser();
|
||||
const endpoint = useCurrentTenantEndpoint();
|
||||
|
||||
const enterAdminConsole = async () => {
|
||||
await update({ isOnboardingDone: true });
|
||||
|
@ -136,114 +152,122 @@ function SignInExperience() {
|
|||
}
|
||||
|
||||
return (
|
||||
<div className={pageLayout.page}>
|
||||
<PageMeta titleKey={['cloud.sie.page_title', 'cloud.general.onboarding']} />
|
||||
<OverlayScrollbar className={pageLayout.contentContainer}>
|
||||
<div className={styles.content}>
|
||||
<div className={styles.config}>
|
||||
<Tools />
|
||||
<div className={pageLayout.title}>{t('cloud.sie.title')}</div>
|
||||
<InspireMe
|
||||
onInspired={(template) => {
|
||||
for (const [key, value] of Object.entries(template)) {
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
setValue(key as keyof OnboardingSieFormData, value, { shouldDirty: true });
|
||||
}
|
||||
updateAuthenticationConfig();
|
||||
}}
|
||||
/>
|
||||
<FormField title="cloud.sie.logo_field">
|
||||
{isUserAssetsServiceReady ? (
|
||||
<SWRConfig value={swrOptions}>
|
||||
<div className={pageLayout.page}>
|
||||
<PageMeta titleKey={['cloud.sie.page_title', 'cloud.general.onboarding']} />
|
||||
<OverlayScrollbar className={pageLayout.contentContainer}>
|
||||
<div className={styles.content}>
|
||||
<div className={styles.config}>
|
||||
<Tools />
|
||||
<div className={pageLayout.title}>{t('cloud.sie.title')}</div>
|
||||
<InspireMe
|
||||
onInspired={(template) => {
|
||||
for (const [key, value] of Object.entries(template)) {
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
setValue(key as keyof OnboardingSieFormData, value, { shouldDirty: true });
|
||||
}
|
||||
updateAuthenticationConfig();
|
||||
}}
|
||||
/>
|
||||
<FormField title="cloud.sie.logo_field">
|
||||
{isUserAssetsServiceReady ? (
|
||||
<Controller
|
||||
name="logo"
|
||||
control={control}
|
||||
render={({ field: { onChange, value, name } }) => (
|
||||
<ImageUploaderField name={name} value={value ?? ''} onChange={onChange} />
|
||||
)}
|
||||
/>
|
||||
) : (
|
||||
<TextInput
|
||||
{...register('logo', {
|
||||
validate: (value) =>
|
||||
!value || uriValidator(value) || t('errors.invalid_uri_format'),
|
||||
})}
|
||||
error={errors.logo?.message}
|
||||
/>
|
||||
)}
|
||||
</FormField>
|
||||
<FormField title="cloud.sie.color_field">
|
||||
<Controller
|
||||
name="logo"
|
||||
name="color"
|
||||
control={control}
|
||||
render={({ field: { onChange, value, name } }) => (
|
||||
<ImageUploaderField name={name} value={value ?? ''} onChange={onChange} />
|
||||
render={({ field: { onChange, value } }) => (
|
||||
<ColorPicker value={value} onChange={onChange} />
|
||||
)}
|
||||
/>
|
||||
) : (
|
||||
<TextInput
|
||||
{...register('logo', {
|
||||
validate: (value) =>
|
||||
!value || uriValidator(value) || t('errors.invalid_uri_format'),
|
||||
})}
|
||||
error={errors.logo?.message}
|
||||
</FormField>
|
||||
<FormField title="cloud.sie.identifier_field" headlineSpacing="large">
|
||||
<Controller
|
||||
name="identifier"
|
||||
control={control}
|
||||
render={({ field: { name, value, onChange } }) => (
|
||||
<CardSelector
|
||||
name={name}
|
||||
value={value}
|
||||
options={identifierOptions}
|
||||
onChange={(value) => {
|
||||
onChange(value);
|
||||
updateAuthenticationConfig();
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
</FormField>
|
||||
<FormField title="cloud.sie.color_field">
|
||||
<Controller
|
||||
name="color"
|
||||
control={control}
|
||||
render={({ field: { onChange, value } }) => (
|
||||
<ColorPicker value={value} onChange={onChange} />
|
||||
)}
|
||||
</FormField>
|
||||
<FormField isMultiple title="cloud.sie.authn_field" headlineSpacing="large">
|
||||
<Controller
|
||||
name="authentications"
|
||||
control={control}
|
||||
defaultValue={defaultOnboardingSieFormData.authentications}
|
||||
render={({ field: { value, onChange } }) => (
|
||||
<MultiCardSelector
|
||||
isNotAllowEmpty
|
||||
className={styles.authnSelector}
|
||||
value={value}
|
||||
options={authenticationOptions.filter(
|
||||
({ value }) =>
|
||||
onboardingSieFormData.identifier !== SignInIdentifier.Username ||
|
||||
value === Authentication.Password
|
||||
)}
|
||||
onChange={onChange}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</FormField>
|
||||
<FormField isMultiple title="cloud.sie.social_field" headlineSpacing="large">
|
||||
<Controller
|
||||
name="socialTargets"
|
||||
control={control}
|
||||
defaultValue={defaultOnboardingSieFormData.socialTargets}
|
||||
render={({ field: { value, onChange } }) => (
|
||||
<SocialSelector value={value ?? []} onChange={onChange} />
|
||||
)}
|
||||
/>
|
||||
</FormField>
|
||||
</div>
|
||||
{endpoint && (
|
||||
<Preview
|
||||
className={styles.preview}
|
||||
signInExperience={previewSieConfig}
|
||||
endpoint={endpoint}
|
||||
/>
|
||||
</FormField>
|
||||
<FormField title="cloud.sie.identifier_field" headlineSpacing="large">
|
||||
<Controller
|
||||
name="identifier"
|
||||
control={control}
|
||||
render={({ field: { name, value, onChange } }) => (
|
||||
<CardSelector
|
||||
name={name}
|
||||
value={value}
|
||||
options={identifierOptions}
|
||||
onChange={(value) => {
|
||||
onChange(value);
|
||||
updateAuthenticationConfig();
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</FormField>
|
||||
<FormField isMultiple title="cloud.sie.authn_field" headlineSpacing="large">
|
||||
<Controller
|
||||
name="authentications"
|
||||
control={control}
|
||||
defaultValue={defaultOnboardingSieFormData.authentications}
|
||||
render={({ field: { value, onChange } }) => (
|
||||
<MultiCardSelector
|
||||
isNotAllowEmpty
|
||||
className={styles.authnSelector}
|
||||
value={value}
|
||||
options={authenticationOptions.filter(
|
||||
({ value }) =>
|
||||
onboardingSieFormData.identifier !== SignInIdentifier.Username ||
|
||||
value === Authentication.Password
|
||||
)}
|
||||
onChange={onChange}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</FormField>
|
||||
<FormField isMultiple title="cloud.sie.social_field" headlineSpacing="large">
|
||||
<Controller
|
||||
name="socialTargets"
|
||||
control={control}
|
||||
defaultValue={defaultOnboardingSieFormData.socialTargets}
|
||||
render={({ field: { value, onChange } }) => (
|
||||
<SocialSelector value={value ?? []} onChange={onChange} />
|
||||
)}
|
||||
/>
|
||||
</FormField>
|
||||
)}
|
||||
</div>
|
||||
<Preview className={styles.preview} signInExperience={previewSieConfig} />
|
||||
</div>
|
||||
</OverlayScrollbar>
|
||||
<ActionBar step={3} totalSteps={3}>
|
||||
<div className={styles.continueActions}>
|
||||
<Button
|
||||
type="primary"
|
||||
title="cloud.sie.finish_and_done"
|
||||
disabled={isSubmitting}
|
||||
onClick={async () => {
|
||||
await handleSubmit(submit(enterAdminConsole))();
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</ActionBar>
|
||||
</div>
|
||||
</OverlayScrollbar>
|
||||
<ActionBar step={3} totalSteps={3}>
|
||||
<div className={styles.continueActions}>
|
||||
<Button
|
||||
type="primary"
|
||||
title="cloud.sie.finish_and_done"
|
||||
disabled={isSubmitting}
|
||||
onClick={async () => {
|
||||
await handleSubmit(submit(enterAdminConsole))();
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</ActionBar>
|
||||
</div>
|
||||
</SWRConfig>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
Loading…
Add table
Reference in a new issue