@@ -187,10 +182,15 @@ function SamlMetadataFormFields({
}
}
+type SamlMetadataFormProps = {
+ config?: SamlConnectorConfig;
+ providerConfig?: SamlProviderConfig;
+};
+
// Do not show inline notification and parsed config preview if it is on guide page.
function SamlMetadataForm({ config, providerConfig }: SamlMetadataFormProps) {
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' });
- const { setValue } = useFormContext
();
+ const { setValue } = useFormContext();
const identityProviderConfig = providerConfig?.identityProvider;
const isConfigEmpty = !config || Object.keys(config).length === 0;
diff --git a/packages/console/src/pages/EnterpriseSsoDetails/Connection/ServiceProviderInfo/OidcConnectorSpInfo.tsx b/packages/console/src/pages/EnterpriseSsoDetails/Connection/ServiceProviderInfo/OidcConnectorSpInfo.tsx
new file mode 100644
index 000000000..3a1b3704a
--- /dev/null
+++ b/packages/console/src/pages/EnterpriseSsoDetails/Connection/ServiceProviderInfo/OidcConnectorSpInfo.tsx
@@ -0,0 +1,30 @@
+import { useContext } from 'react';
+
+import { AppDataContext } from '@/contexts/AppDataProvider';
+import CopyToClipboard from '@/ds-components/CopyToClipboard';
+import FormField from '@/ds-components/FormField';
+import useCustomDomain from '@/hooks/use-custom-domain';
+
+import * as styles from './index.module.scss';
+
+type Props = {
+ ssoConnectorId: string;
+};
+
+function OidcConnectorSpInfo({ ssoConnectorId }: Props) {
+ const { tenantEndpoint } = useContext(AppDataContext);
+ const { applyDomain: applyCustomDomain } = useCustomDomain();
+
+ return (
+
+ {/* Generated and passed in by Admin console. */}
+
+
+ );
+}
+
+export default OidcConnectorSpInfo;
diff --git a/packages/console/src/pages/EnterpriseSsoDetails/Connection/ServiceProviderInfo/SamlConnectorSpInfo.tsx b/packages/console/src/pages/EnterpriseSsoDetails/Connection/ServiceProviderInfo/SamlConnectorSpInfo.tsx
new file mode 100644
index 000000000..c0de49f3c
--- /dev/null
+++ b/packages/console/src/pages/EnterpriseSsoDetails/Connection/ServiceProviderInfo/SamlConnectorSpInfo.tsx
@@ -0,0 +1,48 @@
+import { conditionalString } from '@silverhand/essentials';
+
+import CopyToClipboard from '@/ds-components/CopyToClipboard';
+import FormField from '@/ds-components/FormField';
+import useCustomDomain from '@/hooks/use-custom-domain';
+
+import { type SamlProviderConfig } from '../../types/saml';
+
+import * as styles from './index.module.scss';
+
+type Props = {
+ samlProviderConfig?: SamlProviderConfig;
+};
+
+function SamlConnectorSpInfo({ samlProviderConfig }: Props) {
+ const { applyDomain: applyCustomDomain } = useCustomDomain();
+
+ /**
+ * Should not fallback to some other manually concatenated URL, show empty string instead.
+ * Empty string should never show up unless the API does not work properly.
+ */
+ return (
+ <>
+
+
+
+
+
+
+ >
+ );
+}
+
+export default SamlConnectorSpInfo;
diff --git a/packages/console/src/pages/EnterpriseSsoDetails/Connection/BasicInfo/index.module.scss b/packages/console/src/pages/EnterpriseSsoDetails/Connection/ServiceProviderInfo/index.module.scss
similarity index 100%
rename from packages/console/src/pages/EnterpriseSsoDetails/Connection/BasicInfo/index.module.scss
rename to packages/console/src/pages/EnterpriseSsoDetails/Connection/ServiceProviderInfo/index.module.scss
diff --git a/packages/console/src/pages/EnterpriseSsoDetails/Connection/index.tsx b/packages/console/src/pages/EnterpriseSsoDetails/Connection/index.tsx
index 0ce4f6971..37ccabbf0 100644
--- a/packages/console/src/pages/EnterpriseSsoDetails/Connection/index.tsx
+++ b/packages/console/src/pages/EnterpriseSsoDetails/Connection/index.tsx
@@ -1,211 +1,39 @@
-import { SsoProviderName, type RequestErrorBody } from '@logto/schemas';
-import { conditional, type Optional } from '@silverhand/essentials';
-import cleanDeep from 'clean-deep';
-import { HTTPError } from 'ky';
-import { useEffect } from 'react';
-import { useForm, FormProvider, type Path } from 'react-hook-form';
-import { toast } from 'react-hot-toast';
-import { useTranslation } from 'react-i18next';
+import { SsoProviderType, type SsoConnectorWithProviderConfig } from '@logto/schemas';
-import DetailsForm from '@/components/DetailsForm';
-import FormCard from '@/components/FormCard';
-import UnsavedChangesAlertModal from '@/components/UnsavedChangesAlertModal';
-import useApi from '@/hooks/use-api';
-import {
- type SsoConnectorWithProviderConfigWithGeneric,
- type ParsedSsoIdentityProviderConfig,
- type GuideFormType,
- type SsoConnectorConfig,
- type SamlGuideFormType,
-} from '@/pages/EnterpriseSso/types';
-import { trySubmitSafe } from '@/utils/form';
+import { type OidcSsoConnectorWithProviderConfig } from '../types/oidc';
+import { type SamlSsoConnectorWithProviderConfig } from '../types/saml';
-import BasicInfo from './BasicInfo';
-import OidcMetadataForm from './OidcMetadataForm';
-import SamlAttributeMapping from './SamlAttributeMapping';
-import SamlMetadataForm from './SamlMetadataForm';
-import * as styles from './index.module.scss';
+import OidcConnectorForm from './OidcConnectorForm';
+import SamlConnectorForm from './SamlConnectorForm';
-type Props = {
+type Props = {
isDeleted: boolean;
- data: SsoConnectorWithProviderConfigWithGeneric;
- onUpdated: (data: SsoConnectorWithProviderConfigWithGeneric) => void;
+ data: SsoConnectorWithProviderConfig;
+ onUpdated: (data: SsoConnectorWithProviderConfig) => void;
};
-const invalidConfigErrorCode = 'connector.invalid_config';
-const invalidMetadataErrorCode = 'connector.invalid_metadata';
+function isSamlProviderData(
+ data: SsoConnectorWithProviderConfig
+): data is SamlSsoConnectorWithProviderConfig {
+ return data.providerType === SsoProviderType.SAML;
+}
-// This component contains only `data.config`.
-function Connection({ isDeleted, data, onUpdated }: Props) {
- const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' });
- const { id: ssoConnectorId, providerName, providerConfig, config } = data;
+function isOidcProviderData(
+ data: SsoConnectorWithProviderConfig
+): data is OidcSsoConnectorWithProviderConfig {
+ return data.providerType === SsoProviderType.OIDC;
+}
- const api = useApi({ hideErrorToast: true });
+function Connection({ isDeleted, data, onUpdated }: Props) {
+ if (isSamlProviderData(data)) {
+ return ;
+ }
- const methods = useForm>();
+ if (isOidcProviderData(data)) {
+ return ;
+ }
- const {
- watch,
- setError,
- formState: { isSubmitting, isDirty },
- handleSubmit,
- reset,
- } = methods;
-
- useEffect(() => {
- reset(config);
- }, [config, reset]);
-
- const onSubmit = handleSubmit(
- trySubmitSafe(async (formData) => {
- if (isSubmitting) {
- return;
- }
-
- try {
- const updatedSsoConnector = await api
- // TODO: @darcyYe add console test case to remove attribute mapping config.
- .patch(`api/sso-connectors/${ssoConnectorId}`, {
- json: { config: cleanDeep(formData) },
- })
- .json>();
-
- toast.success(t('general.saved'));
- onUpdated(updatedSsoConnector);
-
- reset(updatedSsoConnector.config);
- } catch (error: unknown) {
- if (error instanceof HTTPError) {
- const { response } = error;
- const metadata = await response.clone().json();
-
- // TODO: @darcyYe refactor the generic of `GuideFormType`.
- // Typescript can not infer the generic `GuideFormType`, find a better way to deal with the types later.
-
- if (metadata.code === invalidConfigErrorCode) {
- // OIDC-based SSO connector's config only relies on the result of read from `issuer` field.
- if (
- [
- SsoProviderName.OIDC,
- SsoProviderName.GOOGLE_WORKSPACE,
- SsoProviderName.OKTA,
- ].includes(providerName)
- ) {
- // eslint-disable-next-line no-restricted-syntax
- setError('issuer' as Path>, {
- type: 'custom',
- message: metadata.message,
- });
- }
-
- // OIDC-based config has been excluded in previous condition check.
- // eslint-disable-next-line no-restricted-syntax
- const formConfig = watch() as SamlGuideFormType;
- const key =
- conditional(formConfig.metadata && 'metadata') ??
- conditional(formConfig.metadataUrl && 'metadataUrl');
- if (key) {
- // eslint-disable-next-line no-restricted-syntax
- setError(key as Path>, {
- type: 'custom',
- message: metadata.message,
- });
- }
- }
-
- // Invalid metadata error only happens for SAML based SSO connectors, when trying to init IdP with XML-format metadata.
- if (
- metadata.code === invalidMetadataErrorCode &&
- [SsoProviderName.SAML, SsoProviderName.AZURE_AD].includes(providerName)
- ) {
- // Typescript can not infer the generic of setError() path.
- // eslint-disable-next-line no-restricted-syntax
- const formConfig = watch() as SamlGuideFormType;
- const key =
- conditional(formConfig.metadata && 'metadata') ??
- conditional(formConfig.metadataUrl && 'metadataUrl');
- // eslint-disable-next-line no-restricted-syntax
- setError(key as Path>, { type: 'custom', message: metadata.message });
- }
- }
-
- throw error;
- }
- })
- );
-
- return (
-
-
- {[SsoProviderName.OIDC, SsoProviderName.GOOGLE_WORKSPACE, SsoProviderName.OKTA].includes(
- providerName
- ) ? (
-
- {/* Can not infer the type by narrowing down the value of `providerName`, so we need to cast it. */}
- }
- providerConfig={
- // eslint-disable-next-line no-restricted-syntax
- providerConfig as Optional>
- }
- />
-
- ) : (
-
- {/* Can not infer the type by narrowing down the value of `providerName`, so we need to cast it. */}
- {/* Modify spacing between form fields and switch button of SAML metadata form. */}
-
-
}
- providerConfig={
- // eslint-disable-next-line no-restricted-syntax
- providerConfig as Optional>
- }
- />
-
-
- )}
-
-
-
- {[SsoProviderName.SAML, SsoProviderName.AZURE_AD].includes(providerName) && (
-
-
-
- )}
-
-
-
- );
+ return null;
}
export default Connection;
diff --git a/packages/console/src/pages/EnterpriseSsoDetails/config.ts b/packages/console/src/pages/EnterpriseSsoDetails/config.ts
new file mode 100644
index 000000000..00f44620e
--- /dev/null
+++ b/packages/console/src/pages/EnterpriseSsoDetails/config.ts
@@ -0,0 +1,3 @@
+export const enterpriseSsoPathname = '/enterprise-sso';
+export const invalidConfigErrorCode = 'connector.invalid_config';
+export const invalidMetadataErrorCode = 'connector.invalid_metadata';
diff --git a/packages/console/src/pages/EnterpriseSsoDetails/index.tsx b/packages/console/src/pages/EnterpriseSsoDetails/index.tsx
index a17228469..9351609be 100644
--- a/packages/console/src/pages/EnterpriseSsoDetails/index.tsx
+++ b/packages/console/src/pages/EnterpriseSsoDetails/index.tsx
@@ -1,11 +1,9 @@
import { withAppInsights } from '@logto/app-insights/react';
-import { type SsoProviderName, type SignInExperience } from '@logto/schemas';
+import { type SignInExperience, type SsoConnectorWithProviderConfig } from '@logto/schemas';
import { pick } from '@silverhand/essentials';
import { useEffect, useState } from 'react';
-import { toast } from 'react-hot-toast';
-import { useTranslation } from 'react-i18next';
import { useLocation, useParams } from 'react-router-dom';
-import useSWR, { useSWRConfig } from 'swr';
+import useSWR from 'swr';
import Delete from '@/assets/icons/delete.svg';
import File from '@/assets/icons/file.svg';
@@ -19,52 +17,46 @@ import ConfirmModal from '@/ds-components/ConfirmModal';
import DynamicT from '@/ds-components/DynamicT';
import TabNav, { TabNavItem } from '@/ds-components/TabNav';
import type { RequestError } from '@/hooks/use-api';
-import useApi from '@/hooks/use-api';
-import useTenantPathname from '@/hooks/use-tenant-pathname';
import useUserAssetsService from '@/hooks/use-user-assets-service';
import SsoConnectorLogo from '../EnterpriseSso/SsoConnectorLogo';
-import { type SsoConnectorWithProviderConfigWithGeneric } from '../EnterpriseSso/types';
import Connection from './Connection';
import Experience from './Experience';
import SsoGuide from './SsoGuide';
+import { enterpriseSsoPathname } from './config';
import * as styles from './index.module.scss';
+import useDeleteConnector from './use-delete-connector';
-const enterpriseSsoPathname = '/enterprise-sso';
const getSsoConnectorDetailsPathname = (ssoConnectorId: string, tab: EnterpriseSsoDetailsTabs) =>
`${enterpriseSsoPathname}/${ssoConnectorId}/${tab}`;
-function EnterpriseSsoConnectorDetails() {
+function EnterpriseSsoConnectorDetails() {
const { pathname } = useLocation();
const { ssoConnectorId, tab } = useParams();
- const { mutate: mutateGlobal } = useSWRConfig();
- const [isDeleted, setIsDeleted] = useState(false);
+ const { isDeleted, isDeleting, onDeleteHandler } = useDeleteConnector(ssoConnectorId);
+
const [isReadmeOpen, setIsReadmeOpen] = useState(false);
const { isLoading: isUserAssetServiceLoading } = useUserAssetsService();
+
const { data: signInExperience, isLoading: isSignInExperienceLoading } =
useSWR('api/sign-in-exp');
- const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' });
const {
data: ssoConnector,
error: requestError,
mutate,
isLoading: isSsoConnectorLoading,
- } = useSWR, RequestError>(
+ } = useSWR(
ssoConnectorId && `api/sso-connectors/${ssoConnectorId}`,
{ keepPreviousData: true }
);
const isLoading = isSsoConnectorLoading || isUserAssetServiceLoading || isSignInExperienceLoading;
- const api = useApi();
- const { navigate } = useTenantPathname();
-
const [isDeleteAlertOpen, setIsDeleteAlertOpen] = useState(false);
- const [isDeleting, setIsDeleting] = useState(false);
const isDarkModeEnabled = signInExperience?.color.isDarkModeEnabled ?? false;
@@ -72,27 +64,6 @@ function EnterpriseSsoConnectorDetails() {
setIsDeleteAlertOpen(false);
}, [pathname]);
- const handleDelete = async () => {
- if (!ssoConnectorId || isDeleting) {
- return;
- }
- setIsDeleting(true);
-
- try {
- await api
- .delete(`api/sso-connectors/${ssoConnectorId}`)
- .json>();
-
- setIsDeleted(true);
-
- toast.success(t('enterprise_sso_details.enterprise_sso_deleted'));
- await mutateGlobal('api/sso-connectors');
- navigate(enterpriseSsoPathname, { replace: true });
- } finally {
- setIsDeleting(false);
- }
- };
-
if (!ssoConnectorId) {
return null;
}
@@ -194,7 +165,7 @@ function EnterpriseSsoConnectorDetails() {
onCancel={async () => {
setIsDeleteAlertOpen(false);
}}
- onConfirm={handleDelete}
+ onConfirm={onDeleteHandler}
>
diff --git a/packages/console/src/pages/EnterpriseSsoDetails/types/oidc.ts b/packages/console/src/pages/EnterpriseSsoDetails/types/oidc.ts
new file mode 100644
index 000000000..c366b5dfc
--- /dev/null
+++ b/packages/console/src/pages/EnterpriseSsoDetails/types/oidc.ts
@@ -0,0 +1,36 @@
+import { type SsoProviderType, type SsoConnectorWithProviderConfig } from '@logto/schemas';
+import { z } from 'zod';
+
+/* Oidc Connectors */
+export type OidcSsoConnectorWithProviderConfig = Omit<
+ SsoConnectorWithProviderConfig,
+ 'providerType'
+> & {
+ providerType: SsoProviderType.OIDC;
+};
+
+/**
+ * All the following guards are copied from {@link @logto/core/packages/core/src/sso/types/oidc }
+ * @TODO: consider to move them to a shared package e.g. @logto/schemas
+ */
+
+export const oidcConnectorConfigGuard = z
+ .object({
+ clientId: z.string(),
+ clientSecret: z.string(),
+ issuer: z.string(),
+ scope: z.string().optional(),
+ })
+ .partial();
+
+export type OidcConnectorConfig = z.infer;
+
+export const oidcProviderConfigGuard = z.object({
+ authorizationEndpoint: z.string(),
+ tokenEndpoint: z.string(),
+ userinfoEndpoint: z.string(),
+ jwksUri: z.string(),
+ issuer: z.string(),
+});
+
+export type OidcProviderConfig = z.infer;
diff --git a/packages/console/src/pages/EnterpriseSsoDetails/types/saml.ts b/packages/console/src/pages/EnterpriseSsoDetails/types/saml.ts
new file mode 100644
index 000000000..0df241363
--- /dev/null
+++ b/packages/console/src/pages/EnterpriseSsoDetails/types/saml.ts
@@ -0,0 +1,64 @@
+import { type SsoConnectorWithProviderConfig, type SsoProviderType } from '@logto/schemas';
+import { z } from 'zod';
+
+/* Saml Connectors */
+export type SamlSsoConnectorWithProviderConfig = Omit<
+ SsoConnectorWithProviderConfig,
+ 'providerType'
+> & {
+ providerType: SsoProviderType.SAML;
+};
+
+/**
+ * All the following guards are copied from {@link @logto/core/packages/core/src/sso/types/saml }
+ * @TODO: consider to move them to a shared package e.g. @logto/schemas
+ */
+const samlAttributeMappingGuard = z
+ .object({
+ id: z.string(),
+ email: z.string(),
+ name: z.string(),
+ })
+ .partial();
+
+type SamlAttributeMapping = z.infer;
+
+export const samlAttributeKeys = Object.freeze(['id', 'email', 'name']) satisfies ReadonlyArray<
+ keyof SamlAttributeMapping
+>;
+
+// Guard the saml connector config data from the response of the API.
+export const samlConnectorConfigGuard = z
+ .object({
+ metadataUrl: z.string(),
+ metadata: z.string(),
+ signInEndpoint: z.string(),
+ entityId: z.string(),
+ x509Certificate: z.string(),
+ attributeMapping: samlAttributeMappingGuard,
+ })
+ .partial();
+
+export type SamlConnectorConfig = z.infer;
+
+// Guard the saml provider config from the response of the API.
+const samlServiceProviderMetadataGuard = z.object({
+ entityId: z.string().min(1),
+ assertionConsumerServiceUrl: z.string().min(1),
+});
+
+const samlIdentityProviderMetadataGuard = z.object({
+ entityId: z.string(),
+ signInEndpoint: z.string(),
+ x509Certificate: z.string(),
+ certificateExpiresAt: z.number(), // Timestamp in milliseconds.
+ isCertificateValid: z.boolean(),
+});
+
+export const samlProviderConfigGuard = z.object({
+ defaultAttributeMapping: samlAttributeMappingGuard,
+ serviceProvider: samlServiceProviderMetadataGuard,
+ identityProvider: samlIdentityProviderMetadataGuard.optional(),
+});
+
+export type SamlProviderConfig = z.infer;
diff --git a/packages/console/src/pages/EnterpriseSsoDetails/use-delete-connector.ts b/packages/console/src/pages/EnterpriseSsoDetails/use-delete-connector.ts
new file mode 100644
index 000000000..15e1a1869
--- /dev/null
+++ b/packages/console/src/pages/EnterpriseSsoDetails/use-delete-connector.ts
@@ -0,0 +1,50 @@
+import { useCallback, useState } from 'react';
+import { toast } from 'react-hot-toast';
+import { useTranslation } from 'react-i18next';
+import { useSWRConfig } from 'swr';
+
+import useApi from '@/hooks/use-api';
+import useTenantPathname from '@/hooks/use-tenant-pathname';
+
+import { enterpriseSsoPathname } from './config';
+
+function useDeleteConnector(ssoConnectorId?: string) {
+ const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' });
+
+ const [isDeleted, setIsDeleted] = useState(false);
+ const [isDeleting, setIsDeleting] = useState(false);
+
+ const api = useApi();
+ const { mutate: mutateGlobal } = useSWRConfig();
+ const { navigate } = useTenantPathname();
+
+ const onDeleteHandler = useCallback(async () => {
+ if (!ssoConnectorId || isDeleting) {
+ return;
+ }
+
+ setIsDeleting(true);
+
+ try {
+ await api.delete(`api/sso-connectors/${ssoConnectorId}`);
+ setIsDeleted(true);
+
+ toast.success(t('enterprise_sso_details.enterprise_sso_deleted'));
+
+ // Reset the sso-connectors data to refresh the list.
+ await mutateGlobal('api/sso-connectors');
+
+ navigate(enterpriseSsoPathname, { replace: true });
+ } finally {
+ setIsDeleting(false);
+ }
+ }, [api, isDeleting, mutateGlobal, navigate, ssoConnectorId, t]);
+
+ return {
+ isDeleted,
+ isDeleting,
+ onDeleteHandler,
+ };
+}
+
+export default useDeleteConnector;
diff --git a/packages/phrases/src/locales/de/translation/admin-console/upsell/paywall.ts b/packages/phrases/src/locales/de/translation/admin-console/upsell/paywall.ts
index b07b09356..52a435b73 100644
--- a/packages/phrases/src/locales/de/translation/admin-console/upsell/paywall.ts
+++ b/packages/phrases/src/locales/de/translation/admin-console/upsell/paywall.ts
@@ -55,6 +55,9 @@ const paywall = {
/** UNTRANSLATED */
third_party_apps:
'Unlock Logto as IdP for third-party apps by upgrading to a paid plan. For any assistance, feel free to contact us.',
+ /** UNTRANSLATED */
+ sso_connectors:
+ 'Unlock enterprise sso by upgrading to a paid plan. For any assistance, feel free to contact us.',
};
export default Object.freeze(paywall);
diff --git a/packages/phrases/src/locales/en/translation/admin-console/upsell/paywall.ts b/packages/phrases/src/locales/en/translation/admin-console/upsell/paywall.ts
index a77d7afb6..93030983c 100644
--- a/packages/phrases/src/locales/en/translation/admin-console/upsell/paywall.ts
+++ b/packages/phrases/src/locales/en/translation/admin-console/upsell/paywall.ts
@@ -54,6 +54,8 @@ const paywall = {
'Unlock organizations by upgrading to a paid plan. Don’t hesitate to contact us if you need any assistance.',
third_party_apps:
'Unlock Logto as IdP for third-party apps by upgrading to a paid plan. For any assistance, feel free to contact us.',
+ sso_connectors:
+ 'Unlock enterprise sso by upgrading to a paid plan. For any assistance, feel free to contact us.',
};
export default Object.freeze(paywall);
diff --git a/packages/phrases/src/locales/es/translation/admin-console/upsell/paywall.ts b/packages/phrases/src/locales/es/translation/admin-console/upsell/paywall.ts
index c167df086..3df71345d 100644
--- a/packages/phrases/src/locales/es/translation/admin-console/upsell/paywall.ts
+++ b/packages/phrases/src/locales/es/translation/admin-console/upsell/paywall.ts
@@ -55,6 +55,9 @@ const paywall = {
/** UNTRANSLATED */
third_party_apps:
'Unlock Logto as IdP for third-party apps by upgrading to a paid plan. For any assistance, feel free to contact us.',
+ /** UNTRANSLATED */
+ sso_connectors:
+ 'Unlock enterprise sso by upgrading to a paid plan. For any assistance, feel free to contact us.',
};
export default Object.freeze(paywall);
diff --git a/packages/phrases/src/locales/fr/translation/admin-console/upsell/paywall.ts b/packages/phrases/src/locales/fr/translation/admin-console/upsell/paywall.ts
index 17dcfcf76..e1ff37316 100644
--- a/packages/phrases/src/locales/fr/translation/admin-console/upsell/paywall.ts
+++ b/packages/phrases/src/locales/fr/translation/admin-console/upsell/paywall.ts
@@ -55,6 +55,9 @@ const paywall = {
/** UNTRANSLATED */
third_party_apps:
'Unlock Logto as IdP for third-party apps by upgrading to a paid plan. For any assistance, feel free to contact us.',
+ /** UNTRANSLATED */
+ sso_connectors:
+ 'Unlock enterprise sso by upgrading to a paid plan. For any assistance, feel free to contact us.',
};
export default Object.freeze(paywall);
diff --git a/packages/phrases/src/locales/it/translation/admin-console/upsell/paywall.ts b/packages/phrases/src/locales/it/translation/admin-console/upsell/paywall.ts
index f4c3dee77..76fbe24de 100644
--- a/packages/phrases/src/locales/it/translation/admin-console/upsell/paywall.ts
+++ b/packages/phrases/src/locales/it/translation/admin-console/upsell/paywall.ts
@@ -55,6 +55,9 @@ const paywall = {
/** UNTRANSLATED */
third_party_apps:
'Unlock Logto as IdP for third-party apps by upgrading to a paid plan. For any assistance, feel free to contact us.',
+ /** UNTRANSLATED */
+ sso_connectors:
+ 'Unlock enterprise sso by upgrading to a paid plan. For any assistance, feel free to contact us.',
};
export default Object.freeze(paywall);
diff --git a/packages/phrases/src/locales/ja/translation/admin-console/upsell/paywall.ts b/packages/phrases/src/locales/ja/translation/admin-console/upsell/paywall.ts
index b00d144f3..ecd6df925 100644
--- a/packages/phrases/src/locales/ja/translation/admin-console/upsell/paywall.ts
+++ b/packages/phrases/src/locales/ja/translation/admin-console/upsell/paywall.ts
@@ -55,6 +55,9 @@ const paywall = {
/** UNTRANSLATED */
third_party_apps:
'Unlock Logto as IdP for third-party apps by upgrading to a paid plan. For any assistance, feel free to contact us.',
+ /** UNTRANSLATED */
+ sso_connectors:
+ 'Unlock enterprise sso by upgrading to a paid plan. For any assistance, feel free to contact us.',
};
export default Object.freeze(paywall);
diff --git a/packages/phrases/src/locales/ko/translation/admin-console/upsell/paywall.ts b/packages/phrases/src/locales/ko/translation/admin-console/upsell/paywall.ts
index f53601e7a..3373b6ab6 100644
--- a/packages/phrases/src/locales/ko/translation/admin-console/upsell/paywall.ts
+++ b/packages/phrases/src/locales/ko/translation/admin-console/upsell/paywall.ts
@@ -55,6 +55,9 @@ const paywall = {
/** UNTRANSLATED */
third_party_apps:
'Unlock Logto as IdP for third-party apps by upgrading to a paid plan. For any assistance, feel free to contact us.',
+ /** UNTRANSLATED */
+ sso_connectors:
+ 'Unlock enterprise sso by upgrading to a paid plan. For any assistance, feel free to contact us.',
};
export default Object.freeze(paywall);
diff --git a/packages/phrases/src/locales/pl-pl/translation/admin-console/upsell/paywall.ts b/packages/phrases/src/locales/pl-pl/translation/admin-console/upsell/paywall.ts
index 97ffd8e93..ffd193ca4 100644
--- a/packages/phrases/src/locales/pl-pl/translation/admin-console/upsell/paywall.ts
+++ b/packages/phrases/src/locales/pl-pl/translation/admin-console/upsell/paywall.ts
@@ -55,6 +55,9 @@ const paywall = {
/** UNTRANSLATED */
third_party_apps:
'Unlock Logto as IdP for third-party apps by upgrading to a paid plan. For any assistance, feel free to contact us.',
+ /** UNTRANSLATED */
+ sso_connectors:
+ 'Unlock enterprise sso by upgrading to a paid plan. For any assistance, feel free to contact us.',
};
export default Object.freeze(paywall);
diff --git a/packages/phrases/src/locales/pt-br/translation/admin-console/upsell/paywall.ts b/packages/phrases/src/locales/pt-br/translation/admin-console/upsell/paywall.ts
index 4e525000d..e3cc12b73 100644
--- a/packages/phrases/src/locales/pt-br/translation/admin-console/upsell/paywall.ts
+++ b/packages/phrases/src/locales/pt-br/translation/admin-console/upsell/paywall.ts
@@ -55,6 +55,9 @@ const paywall = {
/** UNTRANSLATED */
third_party_apps:
'Unlock Logto as IdP for third-party apps by upgrading to a paid plan. For any assistance, feel free to contact us.',
+ /** UNTRANSLATED */
+ sso_connectors:
+ 'Unlock enterprise sso by upgrading to a paid plan. For any assistance, feel free to contact us.',
};
export default Object.freeze(paywall);
diff --git a/packages/phrases/src/locales/pt-pt/translation/admin-console/upsell/paywall.ts b/packages/phrases/src/locales/pt-pt/translation/admin-console/upsell/paywall.ts
index 66e36fe7d..1243b2a90 100644
--- a/packages/phrases/src/locales/pt-pt/translation/admin-console/upsell/paywall.ts
+++ b/packages/phrases/src/locales/pt-pt/translation/admin-console/upsell/paywall.ts
@@ -55,6 +55,9 @@ const paywall = {
/** UNTRANSLATED */
third_party_apps:
'Unlock Logto as IdP for third-party apps by upgrading to a paid plan. For any assistance, feel free to contact us.',
+ /** UNTRANSLATED */
+ sso_connectors:
+ 'Unlock enterprise sso by upgrading to a paid plan. For any assistance, feel free to contact us.',
};
export default Object.freeze(paywall);
diff --git a/packages/phrases/src/locales/ru/translation/admin-console/upsell/paywall.ts b/packages/phrases/src/locales/ru/translation/admin-console/upsell/paywall.ts
index 6763cdc3e..6d13ad100 100644
--- a/packages/phrases/src/locales/ru/translation/admin-console/upsell/paywall.ts
+++ b/packages/phrases/src/locales/ru/translation/admin-console/upsell/paywall.ts
@@ -55,6 +55,9 @@ const paywall = {
/** UNTRANSLATED */
third_party_apps:
'Unlock Logto as IdP for third-party apps by upgrading to a paid plan. For any assistance, feel free to contact us.',
+ /** UNTRANSLATED */
+ sso_connectors:
+ 'Unlock enterprise sso by upgrading to a paid plan. For any assistance, feel free to contact us.',
};
export default Object.freeze(paywall);
diff --git a/packages/phrases/src/locales/tr-tr/translation/admin-console/upsell/paywall.ts b/packages/phrases/src/locales/tr-tr/translation/admin-console/upsell/paywall.ts
index 4d320c96a..e1750692b 100644
--- a/packages/phrases/src/locales/tr-tr/translation/admin-console/upsell/paywall.ts
+++ b/packages/phrases/src/locales/tr-tr/translation/admin-console/upsell/paywall.ts
@@ -55,6 +55,9 @@ const paywall = {
/** UNTRANSLATED */
third_party_apps:
'Unlock Logto as IdP for third-party apps by upgrading to a paid plan. For any assistance, feel free to contact us.',
+ /** UNTRANSLATED */
+ sso_connectors:
+ 'Unlock enterprise sso by upgrading to a paid plan. For any assistance, feel free to contact us.',
};
export default Object.freeze(paywall);
diff --git a/packages/phrases/src/locales/zh-cn/translation/admin-console/upsell/paywall.ts b/packages/phrases/src/locales/zh-cn/translation/admin-console/upsell/paywall.ts
index 50a9095c7..87b87fe6d 100644
--- a/packages/phrases/src/locales/zh-cn/translation/admin-console/upsell/paywall.ts
+++ b/packages/phrases/src/locales/zh-cn/translation/admin-console/upsell/paywall.ts
@@ -54,6 +54,9 @@ const paywall = {
/** UNTRANSLATED */
third_party_apps:
'Unlock Logto as IdP for third-party apps by upgrading to a paid plan. For any assistance, feel free to contact us.',
+ /** UNTRANSLATED */
+ sso_connectors:
+ 'Unlock enterprise sso by upgrading to a paid plan. For any assistance, feel free to contact us.',
};
export default Object.freeze(paywall);
diff --git a/packages/phrases/src/locales/zh-hk/translation/admin-console/upsell/paywall.ts b/packages/phrases/src/locales/zh-hk/translation/admin-console/upsell/paywall.ts
index 0d565bf63..be7775432 100644
--- a/packages/phrases/src/locales/zh-hk/translation/admin-console/upsell/paywall.ts
+++ b/packages/phrases/src/locales/zh-hk/translation/admin-console/upsell/paywall.ts
@@ -54,6 +54,9 @@ const paywall = {
/** UNTRANSLATED */
third_party_apps:
'Unlock Logto as IdP for third-party apps by upgrading to a paid plan. For any assistance, feel free to contact us.',
+ /** UNTRANSLATED */
+ sso_connectors:
+ 'Unlock enterprise sso by upgrading to a paid plan. For any assistance, feel free to contact us.',
};
export default Object.freeze(paywall);
diff --git a/packages/phrases/src/locales/zh-tw/translation/admin-console/upsell/paywall.ts b/packages/phrases/src/locales/zh-tw/translation/admin-console/upsell/paywall.ts
index a8316b11a..9cd5b878b 100644
--- a/packages/phrases/src/locales/zh-tw/translation/admin-console/upsell/paywall.ts
+++ b/packages/phrases/src/locales/zh-tw/translation/admin-console/upsell/paywall.ts
@@ -54,6 +54,9 @@ const paywall = {
/** UNTRANSLATED */
third_party_apps:
'Unlock Logto as IdP for third-party apps by upgrading to a paid plan. For any assistance, feel free to contact us.',
+ /** UNTRANSLATED */
+ sso_connectors:
+ 'Unlock enterprise sso by upgrading to a paid plan. For any assistance, feel free to contact us.',
};
export default Object.freeze(paywall);