diff --git a/packages/console/src/components/CustomUiAssetsUploader/index.tsx b/packages/console/src/components/CustomUiAssetsUploader/index.tsx index 79b240d81..4a1e290b8 100644 --- a/packages/console/src/components/CustomUiAssetsUploader/index.tsx +++ b/packages/console/src/components/CustomUiAssetsUploader/index.tsx @@ -15,11 +15,13 @@ import * as styles from './index.module.scss'; type Props = { readonly value?: CustomUiAssets; readonly onChange: (value: CustomUiAssets) => void; + // eslint-disable-next-line react/boolean-prop-naming + readonly disabled?: boolean; }; const allowedMimeTypes: AllowedUploadMimeType[] = ['application/zip']; -function CustomUiAssetsUploader({ value, onChange }: Props) { +function CustomUiAssetsUploader({ value, onChange, disabled }: Props) { const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' }); const [file, setFile] = useState(); const [error, setError] = useState(); @@ -48,6 +50,7 @@ function CustomUiAssetsUploader({ value, onChange }: Props) { if (showUploader) { return ( + disabled={disabled} allowedMimeTypes={allowedMimeTypes} maxSize={maxUploadFileSize} uploadUrl="api/sign-in-exp/default/custom-ui-assets" diff --git a/packages/console/src/components/FeatureTag/index.tsx b/packages/console/src/components/FeatureTag/index.tsx index 6363a5851..cfec1dc52 100644 --- a/packages/console/src/components/FeatureTag/index.tsx +++ b/packages/console/src/components/FeatureTag/index.tsx @@ -8,7 +8,7 @@ import * as styles from './index.module.scss'; export { default as BetaTag } from './BetaTag'; -type Props = { +export type Props = { /** * Whether the tag should be visible. It should be `true` if the tenant's subscription * plan has NO access to the feature (paywall), but it will always be visible for dev diff --git a/packages/console/src/ds-components/FormField/index.module.scss b/packages/console/src/ds-components/FormField/index.module.scss index 0a9a63254..ed38b54a9 100644 --- a/packages/console/src/ds-components/FormField/index.module.scss +++ b/packages/console/src/ds-components/FormField/index.module.scss @@ -34,6 +34,10 @@ font: var(--font-body-2); color: var(--color-text-secondary); } + + .featureTag { + margin-left: _.unit(2); + } } .description { diff --git a/packages/console/src/ds-components/FormField/index.tsx b/packages/console/src/ds-components/FormField/index.tsx index d9ac103e7..1a2ada001 100644 --- a/packages/console/src/ds-components/FormField/index.tsx +++ b/packages/console/src/ds-components/FormField/index.tsx @@ -4,6 +4,7 @@ import type { ReactElement, ReactNode } from 'react'; import { useTranslation } from 'react-i18next'; import Tip from '@/assets/icons/tip.svg'; +import FeatureTag, { type Props as FeatureTagProps } from '@/components/FeatureTag'; import type DangerousRaw from '../DangerousRaw'; import DynamicT from '../DynamicT'; @@ -25,6 +26,7 @@ export type Props = { readonly headlineSpacing?: 'default' | 'large'; readonly headlineClassName?: string; readonly tip?: ToggleTipProps['content']; + readonly featureTag?: FeatureTagProps; }; function FormField({ @@ -37,6 +39,7 @@ function FormField({ className, headlineSpacing = 'default', tip, + featureTag, headlineClassName, }: Props) { const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' }); @@ -63,6 +66,7 @@ function FormField({ )} + {featureTag && } {isRequired &&
{t('general.required')}
} diff --git a/packages/console/src/ds-components/Uploader/FileUploader/index.module.scss b/packages/console/src/ds-components/Uploader/FileUploader/index.module.scss index 77569e882..4aaef09ab 100644 --- a/packages/console/src/ds-components/Uploader/FileUploader/index.module.scss +++ b/packages/console/src/ds-components/Uploader/FileUploader/index.module.scss @@ -33,7 +33,19 @@ } } - &:hover { + &.disabled { + cursor: not-allowed; + border-color: var(--color-disabled); + color: var(--color-disabled); + + .placeholder { + .icon { + color: var(--color-disabled); + } + } + } + + &:not(.disabled):hover { cursor: pointer; border-color: var(--color-primary); @@ -44,7 +56,7 @@ } } - &.dragActive { + &:not(.disabled).dragActive { cursor: copy; background-color: var(--color-hover-variant); border-color: var(--color-primary); diff --git a/packages/console/src/ds-components/Uploader/FileUploader/index.tsx b/packages/console/src/ds-components/Uploader/FileUploader/index.tsx index e9ea1415c..4e067723f 100644 --- a/packages/console/src/ds-components/Uploader/FileUploader/index.tsx +++ b/packages/console/src/ds-components/Uploader/FileUploader/index.tsx @@ -15,6 +15,8 @@ import { Ring } from '../../Spinner'; import * as styles from './index.module.scss'; export type Props = UserAssets> = { + // eslint-disable-next-line react/boolean-prop-naming + readonly disabled?: boolean; readonly maxSize: number; // In bytes readonly allowedMimeTypes: AllowedUploadMimeType[]; readonly actionDescription?: string; @@ -33,6 +35,7 @@ export type Props = UserAssets> = { }; function FileUploader = UserAssets>({ + disabled, maxSize, allowedMimeTypes, actionDescription, @@ -121,7 +124,7 @@ function FileUploader = UserAssets>({ const { getRootProps, getInputProps, isDragActive } = useDropzone({ onDrop, - disabled: isUploading, + disabled: isUploading || disabled, multiple: false, accept: Object.fromEntries(allowedMimeTypes.map((mimeType) => [mimeType, []])), }); @@ -133,6 +136,7 @@ function FileUploader = UserAssets>({ styles.uploader, Boolean(uploadError) && styles.uploaderError, isDragActive && styles.dragActive, + disabled && styles.disabled, className )} > diff --git a/packages/console/src/pages/SignInExperience/PageContent/Branding/CustomUiForm/index.module.scss b/packages/console/src/pages/SignInExperience/PageContent/Branding/CustomUiForm/index.module.scss index a423521c3..8ca9f7e32 100644 --- a/packages/console/src/pages/SignInExperience/PageContent/Branding/CustomUiForm/index.module.scss +++ b/packages/console/src/pages/SignInExperience/PageContent/Branding/CustomUiForm/index.module.scss @@ -1,4 +1,10 @@ +@use '@/scss/underscore' as _; + .customCssCodeEditor { max-height: calc(100vh - 260px); min-height: 132px; // min-height to show three lines of code } + +.upsell { + margin-top: _.unit(3); +} diff --git a/packages/console/src/pages/SignInExperience/PageContent/Branding/CustomUiForm/index.tsx b/packages/console/src/pages/SignInExperience/PageContent/Branding/CustomUiForm/index.tsx index a5b82f6d0..68185ef5e 100644 --- a/packages/console/src/pages/SignInExperience/PageContent/Branding/CustomUiForm/index.tsx +++ b/packages/console/src/pages/SignInExperience/PageContent/Branding/CustomUiForm/index.tsx @@ -1,8 +1,12 @@ +import { ReservedPlanId } from '@logto/schemas'; +import { useContext } from 'react'; import { Controller, useFormContext } from 'react-hook-form'; import { Trans, useTranslation } from 'react-i18next'; import CustomUiAssetsUploader from '@/components/CustomUiAssetsUploader'; +import InlineUpsell from '@/components/InlineUpsell'; import { isDevFeaturesEnabled } from '@/consts/env'; +import { SubscriptionDataContext } from '@/contexts/SubscriptionDataProvider'; import Card from '@/ds-components/Card'; import CodeEditor from '@/ds-components/CodeEditor'; import FormField from '@/ds-components/FormField'; @@ -18,6 +22,8 @@ function CustomUiForm() { const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' }); const { getDocumentationUrl } = useDocumentationUrl(); const { control } = useFormContext(); + const { currentPlan } = useContext(SubscriptionDataContext); + const isBringYourUiEnabled = currentPlan.quota.bringYourUiEnabled; return ( @@ -79,14 +85,29 @@ function CustomUiForm() { } descriptionPosition="top" + featureTag={{ + isVisible: !isBringYourUiEnabled, + plan: ReservedPlanId.Pro, + }} > ( - + )} /> + {!isBringYourUiEnabled && ( + + )} )} 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 e8cac1dfc..ece1bf8e4 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 @@ -65,6 +65,8 @@ const paywall = { description: "Upgrade to a paid plan for custom JWT functionality and premium benefits. Don't hesitate to contact us if you have any questions.", }, + bring_your_ui: + 'Upgrade to a paid plan for bring your custom UI functionality and premium benefits.', }; export default Object.freeze(paywall);