0
Fork 0
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:
Charles Zhao 2024-07-22 11:50:47 +08:00 committed by GitHub
parent ead7c84973
commit def21c7bca
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 62 additions and 6 deletions

View file

@ -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"

View file

@ -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

View file

@ -34,6 +34,10 @@
font: var(--font-body-2);
color: var(--color-text-secondary);
}
.featureTag {
margin-left: _.unit(2);
}
}
.description {

View file

@ -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>

View file

@ -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);

View file

@ -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
)}
>

View file

@ -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);
}

View file

@ -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>

View file

@ -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);