mirror of
https://github.com/logto-io/logto.git
synced 2024-12-16 20:26:19 -05:00
feat(console,phrases): add bring your UI feature paywall (#6275)
feat(console,phrases): add bring your ui feature paywall
This commit is contained in:
parent
ead7c84973
commit
def21c7bca
9 changed files with 62 additions and 6 deletions
|
@ -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<File>();
|
||||
const [error, setError] = useState<string>();
|
||||
|
@ -48,6 +50,7 @@ function CustomUiAssetsUploader({ value, onChange }: Props) {
|
|||
if (showUploader) {
|
||||
return (
|
||||
<FileUploader<{ customUiAssetId: string }>
|
||||
disabled={disabled}
|
||||
allowedMimeTypes={allowedMimeTypes}
|
||||
maxSize={maxUploadFileSize}
|
||||
uploadUrl="api/sign-in-exp/default/custom-ui-assets"
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -34,6 +34,10 @@
|
|||
font: var(--font-body-2);
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
.featureTag {
|
||||
margin-left: _.unit(2);
|
||||
}
|
||||
}
|
||||
|
||||
.description {
|
||||
|
|
|
@ -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({
|
|||
</IconButton>
|
||||
</ToggleTip>
|
||||
)}
|
||||
{featureTag && <FeatureTag {...featureTag} className={styles.featureTag} />}
|
||||
<Spacer />
|
||||
{isRequired && <div className={styles.required}>{t('general.required')}</div>}
|
||||
</div>
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -15,6 +15,8 @@ import { Ring } from '../../Spinner';
|
|||
import * as styles from './index.module.scss';
|
||||
|
||||
export type Props<T extends Record<string, unknown> = 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<T extends Record<string, unknown> = UserAssets> = {
|
|||
};
|
||||
|
||||
function FileUploader<T extends Record<string, unknown> = UserAssets>({
|
||||
disabled,
|
||||
maxSize,
|
||||
allowedMimeTypes,
|
||||
actionDescription,
|
||||
|
@ -121,7 +124,7 @@ function FileUploader<T extends Record<string, unknown> = 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<T extends Record<string, unknown> = UserAssets>({
|
|||
styles.uploader,
|
||||
Boolean(uploadError) && styles.uploaderError,
|
||||
isDragActive && styles.dragActive,
|
||||
disabled && styles.disabled,
|
||||
className
|
||||
)}
|
||||
>
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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<SignInExperienceForm>();
|
||||
const { currentPlan } = useContext(SubscriptionDataContext);
|
||||
const isBringYourUiEnabled = currentPlan.quota.bringYourUiEnabled;
|
||||
|
||||
return (
|
||||
<Card>
|
||||
|
@ -79,14 +85,29 @@ function CustomUiForm() {
|
|||
</Trans>
|
||||
}
|
||||
descriptionPosition="top"
|
||||
featureTag={{
|
||||
isVisible: !isBringYourUiEnabled,
|
||||
plan: ReservedPlanId.Pro,
|
||||
}}
|
||||
>
|
||||
<Controller
|
||||
name="customUiAssets"
|
||||
control={control}
|
||||
render={({ field: { onChange, value } }) => (
|
||||
<CustomUiAssetsUploader value={value} onChange={onChange} />
|
||||
<CustomUiAssetsUploader
|
||||
disabled={!isBringYourUiEnabled}
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
{!isBringYourUiEnabled && (
|
||||
<InlineUpsell
|
||||
className={brandingStyles.upsell}
|
||||
for="bring_your_ui"
|
||||
actionButtonText="upsell.view_plans"
|
||||
/>
|
||||
)}
|
||||
</FormField>
|
||||
)}
|
||||
</Card>
|
||||
|
|
|
@ -65,6 +65,8 @@ const paywall = {
|
|||
description:
|
||||
"Upgrade to a paid plan for custom JWT functionality and premium benefits. Don't hesitate to <a>contact us</a> 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);
|
||||
|
|
Loading…
Reference in a new issue