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:
parent
9c8b9e4853
commit
bfcc5a2cc9
4 changed files with 47 additions and 8 deletions
|
@ -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;
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -22,6 +22,10 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.unlockMfaNotice {
|
||||||
|
margin-top: _.unit(4);
|
||||||
|
}
|
||||||
|
|
||||||
.policyRadio {
|
.policyRadio {
|
||||||
> div[class$='content'] {
|
> div[class$='content'] {
|
||||||
> div[class$='indicator'] {
|
> div[class$='indicator'] {
|
||||||
|
|
|
@ -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}
|
||||||
|
|
Loading…
Reference in a new issue