0
Fork 0
mirror of https://github.com/logto-io/logto.git synced 2024-12-16 20:26:19 -05:00

feat(console): add quota paywall for mfa feature (#4538)

This commit is contained in:
Xiao Yijun 2023-09-19 12:00:47 +08:00 committed by GitHub
parent 9c8b9e4853
commit bfcc5a2cc9
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 47 additions and 8 deletions

View file

@ -8,11 +8,7 @@ type Props = {
}; };
function ContactUsPhraseLink({ children }: Props) { function ContactUsPhraseLink({ children }: Props) {
return ( return <TextLink href={contactEmailLink}>{children}</TextLink>;
<TextLink href={contactEmailLink} target="_blank">
{children}
</TextLink>
);
} }
export default ContactUsPhraseLink; export default ContactUsPhraseLink;

View file

@ -44,6 +44,11 @@
input:checked + .slider::before { input:checked + .slider::before {
transform: translateX(16px); transform: translateX(16px);
} }
input:disabled + .slider {
background-color: var(--color-disabled);
cursor: not-allowed;
}
} }
.wrapper { .wrapper {

View file

@ -22,6 +22,10 @@
} }
} }
.unlockMfaNotice {
margin-top: _.unit(4);
}
.policyRadio { .policyRadio {
> div[class$='content'] { > div[class$='content'] {
> div[class$='indicator'] { > div[class$='indicator'] {

View file

@ -1,19 +1,24 @@
import { MfaFactor, MfaPolicy, type SignInExperience } from '@logto/schemas'; import { MfaFactor, MfaPolicy, type SignInExperience } from '@logto/schemas';
import classNames from 'classnames'; import classNames from 'classnames';
import { useMemo } from 'react'; import { useContext, useMemo } from 'react';
import { Controller, useForm } from 'react-hook-form'; import { Controller, useForm } from 'react-hook-form';
import { toast } from 'react-hot-toast'; import { toast } from 'react-hot-toast';
import { useTranslation } from 'react-i18next'; import { Trans, useTranslation } from 'react-i18next';
import ContactUsPhraseLink from '@/components/ContactUsPhraseLink';
import DetailsForm from '@/components/DetailsForm'; import DetailsForm from '@/components/DetailsForm';
import FormCard from '@/components/FormCard'; import FormCard from '@/components/FormCard';
import UnsavedChangesAlertModal from '@/components/UnsavedChangesAlertModal'; import UnsavedChangesAlertModal from '@/components/UnsavedChangesAlertModal';
import { isCloud } from '@/consts/env';
import { TenantsContext } from '@/contexts/TenantsProvider';
import DynamicT from '@/ds-components/DynamicT'; import DynamicT from '@/ds-components/DynamicT';
import FormField from '@/ds-components/FormField'; import FormField from '@/ds-components/FormField';
import InlineNotification from '@/ds-components/InlineNotification'; import InlineNotification from '@/ds-components/InlineNotification';
import RadioGroup, { Radio } from '@/ds-components/RadioGroup'; import RadioGroup, { Radio } from '@/ds-components/RadioGroup';
import Switch from '@/ds-components/Switch'; import Switch from '@/ds-components/Switch';
import useApi from '@/hooks/use-api'; import useApi from '@/hooks/use-api';
import useSubscriptionPlan from '@/hooks/use-subscription-plan';
import useTenantPathname from '@/hooks/use-tenant-pathname';
import { trySubmitSafe } from '@/utils/form'; import { trySubmitSafe } from '@/utils/form';
import { type MfaConfigForm, type MfaConfig } from '../types'; import { type MfaConfigForm, type MfaConfig } from '../types';
@ -30,6 +35,11 @@ type Props = {
}; };
function MfaForm({ data, onMfaUpdated }: Props) { function MfaForm({ data, onMfaUpdated }: Props) {
const { currentTenantId } = useContext(TenantsContext);
const { data: currentPlan } = useSubscriptionPlan(currentTenantId);
const { navigate } = useTenantPathname();
const isMfaDisabled = isCloud && !currentPlan?.quota.mfaEnabled;
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' }); const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' });
const { const {
register, register,
@ -80,8 +90,13 @@ function MfaForm({ data, onMfaUpdated }: Props) {
<DynamicT forKey="mfa.multi_factors_description" /> <DynamicT forKey="mfa.multi_factors_description" />
</div> </div>
<div className={styles.factorField}> <div className={styles.factorField}>
<Switch label={<FactorLabel type={MfaFactor.TOTP} />} {...register('totpEnabled')} />
<Switch <Switch
disabled={isMfaDisabled}
label={<FactorLabel type={MfaFactor.TOTP} />}
{...register('totpEnabled')}
/>
<Switch
disabled={isMfaDisabled}
label={<FactorLabel type={MfaFactor.WebAuthn} />} label={<FactorLabel type={MfaFactor.WebAuthn} />}
{...register('webAuthnEnabled')} {...register('webAuthnEnabled')}
/> />
@ -90,6 +105,7 @@ function MfaForm({ data, onMfaUpdated }: Props) {
<DynamicT forKey="mfa.backup_code_setup_hint" /> <DynamicT forKey="mfa.backup_code_setup_hint" />
</div> </div>
<Switch <Switch
disabled={isMfaDisabled}
label={<FactorLabel type={MfaFactor.BackupCode} />} label={<FactorLabel type={MfaFactor.BackupCode} />}
hasError={!isBackupCodeAllowed} hasError={!isBackupCodeAllowed}
{...register('backupCodeEnabled')} {...register('backupCodeEnabled')}
@ -102,6 +118,23 @@ function MfaForm({ data, onMfaUpdated }: Props) {
</div> </div>
</div> </div>
</FormField> </FormField>
{isMfaDisabled && (
<InlineNotification
className={styles.unlockMfaNotice}
action="mfa.view_plans"
onClick={() => {
navigate('/tenant-settings/subscription');
}}
>
<Trans
components={{
a: <ContactUsPhraseLink />,
}}
>
{t('mfa.unlock_reminder')}
</Trans>
</InlineNotification>
)}
</FormCard> </FormCard>
<FormCard title="mfa.policy"> <FormCard title="mfa.policy">
<FormField title="mfa.two_step_sign_in_policy"> <FormField title="mfa.two_step_sign_in_policy">
@ -118,6 +151,7 @@ function MfaForm({ data, onMfaUpdated }: Props) {
return ( return (
<Radio <Radio
key={policy} key={policy}
isDisabled={isMfaDisabled}
className={styles.policyRadio} className={styles.policyRadio}
title={<PolicyOptionTitle {...titleProps} />} title={<PolicyOptionTitle {...titleProps} />}
value={policy} value={policy}