Skip to content

Commit 8011270

Browse files
authored
fix(console): update captcha backlink and always show captcha toggle (#7402)
1 parent 6e69c48 commit 8011270

File tree

7 files changed

+152
-125
lines changed

7 files changed

+152
-125
lines changed

.changeset/perfect-phones-relax.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
---
2+
"@logto/console": patch
3+
---
4+
5+
always show enable CAPTCHA toggle
6+
7+
Even if there is no CAPTCHA provider, the toggle will be shown but disabled.
8+
9+
Also the back link of the captcha details page is changed to `/security/captcha`.

packages/console/src/pages/CaptchaDetails/index.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@ function CaptchaDetails() {
7171

7272
return (
7373
<DetailsPage
74-
backLink="/security"
74+
backLink="/security/captcha"
7575
backLinkTitle="security.captcha_details.back_to_security"
7676
isLoading={isLoading}
7777
error={error}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
@use '@/scss/underscore' as _;
2+
3+
.description {
4+
font: var(--font-body-2);
5+
color: var(--color-text-secondary);
6+
margin-bottom: _.unit(3);
7+
}
Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
import { type CaptchaProvider, type CaptchaPolicy, type SignInExperience } from '@logto/schemas';
2+
import { useContext, useState } from 'react';
3+
import { FormProvider, useForm } from 'react-hook-form';
4+
import { toast } from 'react-hot-toast';
5+
import { useTranslation } from 'react-i18next';
6+
7+
import Plus from '@/assets/icons/plus.svg?react';
8+
import DetailsForm from '@/components/DetailsForm';
9+
import { addOnLabels, CombinedAddOnAndFeatureTag } from '@/components/FeatureTag';
10+
import FormCard from '@/components/FormCard';
11+
import UnsavedChangesAlertModal from '@/components/UnsavedChangesAlertModal';
12+
import { captcha } from '@/consts/external-links';
13+
import { latestProPlanId } from '@/consts/subscriptions';
14+
import { SubscriptionDataContext } from '@/contexts/SubscriptionDataProvider';
15+
import Button from '@/ds-components/Button';
16+
import FormField from '@/ds-components/FormField';
17+
import useApi from '@/hooks/use-api';
18+
import usePaywall from '@/hooks/use-paywall';
19+
import { trySubmitSafe } from '@/utils/form';
20+
21+
import CaptchaCard from './CaptchaCard';
22+
import styles from './CaptchaForm.module.scss';
23+
import CreateCaptchaForm from './CreateCaptchaForm';
24+
import EnableCaptcha from './EnableCaptcha';
25+
26+
type Props = {
27+
readonly captchaProvider?: CaptchaProvider;
28+
readonly formData: CaptchaPolicy;
29+
};
30+
31+
function CaptchaForm({ captchaProvider, formData }: Props) {
32+
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' });
33+
const { isFreeTenant } = usePaywall();
34+
const { mutateSubscriptionQuotaAndUsages } = useContext(SubscriptionDataContext);
35+
36+
const [isCreateCaptchaFormOpen, setIsCreateCaptchaFormOpen] = useState(false);
37+
const formMethods = useForm<CaptchaPolicy>({
38+
defaultValues: formData,
39+
mode: 'onBlur',
40+
});
41+
const {
42+
reset,
43+
handleSubmit,
44+
formState: { isDirty, isSubmitting },
45+
} = formMethods;
46+
const api = useApi();
47+
48+
const onSubmit = trySubmitSafe(async (data: CaptchaPolicy) => {
49+
const { captchaPolicy } = await api
50+
.patch('api/sign-in-exp', {
51+
json: { captchaPolicy: data },
52+
})
53+
.json<SignInExperience>();
54+
reset(captchaPolicy);
55+
mutateSubscriptionQuotaAndUsages();
56+
toast.success(t('general.saved'));
57+
});
58+
59+
return (
60+
<>
61+
<FormProvider {...formMethods}>
62+
<DetailsForm
63+
isDirty={isDirty}
64+
isSubmitting={isSubmitting}
65+
onSubmit={handleSubmit(onSubmit)}
66+
onDiscard={reset}
67+
>
68+
<FormCard
69+
title="security.bot_protection.title"
70+
description="security.bot_protection.description"
71+
learnMoreLink={{ href: captcha }}
72+
tag={
73+
<CombinedAddOnAndFeatureTag
74+
hasAddOnTag
75+
paywall={latestProPlanId}
76+
addOnLabel={addOnLabels.addOnBundle}
77+
/>
78+
}
79+
>
80+
<FormField title="security.bot_protection.captcha.title">
81+
<div className={styles.description}>
82+
{t('security.bot_protection.captcha.placeholder')}
83+
</div>
84+
{isFreeTenant || !captchaProvider ? (
85+
<Button
86+
title="security.bot_protection.captcha.add"
87+
icon={<Plus />}
88+
disabled={isFreeTenant}
89+
onClick={() => {
90+
setIsCreateCaptchaFormOpen(true);
91+
}}
92+
/>
93+
) : (
94+
<CaptchaCard captchaProvider={captchaProvider} />
95+
)}
96+
<EnableCaptcha disabled={isFreeTenant || !captchaProvider} />
97+
</FormField>
98+
</FormCard>
99+
</DetailsForm>
100+
</FormProvider>
101+
<CreateCaptchaForm
102+
isOpen={isCreateCaptchaFormOpen}
103+
onClose={() => {
104+
setIsCreateCaptchaFormOpen(false);
105+
}}
106+
/>
107+
<UnsavedChangesAlertModal hasUnsavedChanges={isDirty} />
108+
</>
109+
);
110+
}
111+
112+
export default CaptchaForm;

packages/console/src/pages/Security/Captcha/EnableCaptcha/index.tsx

Lines changed: 9 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,20 @@
1-
import { type CaptchaPolicy, type SignInExperience } from '@logto/schemas';
2-
import { useEffect } from 'react';
1+
import { type CaptchaPolicy } from '@logto/schemas';
32
import { useFormContext } from 'react-hook-form';
43
import { useTranslation } from 'react-i18next';
5-
import useSWR from 'swr';
64

75
import FormField from '@/ds-components/FormField';
86
import Switch from '@/ds-components/Switch';
9-
import { type RequestError } from '@/hooks/use-api';
107

118
import styles from './index.module.scss';
129

13-
function EnableCaptcha() {
14-
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' });
15-
const { register, reset } = useFormContext<CaptchaPolicy>();
16-
const { data, isLoading } = useSWR<SignInExperience, RequestError>('api/sign-in-exp');
17-
18-
useEffect(() => {
19-
if (data) {
20-
reset(data.captchaPolicy);
21-
}
22-
}, [data, reset]);
10+
type Props = {
11+
// eslint-disable-next-line react/boolean-prop-naming
12+
readonly disabled?: boolean;
13+
};
2314

24-
if (isLoading) {
25-
return null;
26-
}
15+
function EnableCaptcha({ disabled }: Props) {
16+
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' });
17+
const { register } = useFormContext<CaptchaPolicy>();
2718

2819
return (
2920
<div className={styles.container}>
@@ -32,6 +23,7 @@ function EnableCaptcha() {
3223
<Switch
3324
label={t('security.bot_protection.enable_captcha_description')}
3425
{...register('enabled')}
26+
disabled={disabled}
3527
/>
3628
</div>
3729
</FormField>

packages/console/src/pages/Security/Captcha/index.module.scss

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,6 @@
66
flex: 1;
77
}
88

9-
.description {
10-
font: var(--font-body-2);
11-
color: var(--color-text-secondary);
12-
margin-bottom: _.unit(3);
13-
}
14-
159
.upsellNotice {
1610
margin-bottom: _.unit(4);
1711
}

packages/console/src/pages/Security/Captcha/index.tsx

Lines changed: 14 additions & 101 deletions
Original file line numberDiff line numberDiff line change
@@ -1,66 +1,27 @@
1-
import { CaptchaType, type CaptchaPolicy, type SignInExperience } from '@logto/schemas';
2-
import { useContext, useState } from 'react';
3-
import { FormProvider, useForm } from 'react-hook-form';
4-
import { toast } from 'react-hot-toast';
5-
import { useTranslation } from 'react-i18next';
1+
import { CaptchaType, type SignInExperience } from '@logto/schemas';
62
import { useNavigate, useParams } from 'react-router-dom';
3+
import useSWR from 'swr';
74
import { z } from 'zod';
85

9-
import Plus from '@/assets/icons/plus.svg?react';
10-
import DetailsForm from '@/components/DetailsForm';
11-
import { addOnLabels, CombinedAddOnAndFeatureTag } from '@/components/FeatureTag';
12-
import FormCard, { FormCardSkeleton } from '@/components/FormCard';
13-
import UnsavedChangesAlertModal from '@/components/UnsavedChangesAlertModal';
14-
import { captcha } from '@/consts/external-links';
15-
import { latestProPlanId } from '@/consts/subscriptions';
16-
import { SubscriptionDataContext } from '@/contexts/SubscriptionDataProvider';
17-
import Button from '@/ds-components/Button';
18-
import FormField from '@/ds-components/FormField';
19-
import useApi from '@/hooks/use-api';
20-
import usePaywall from '@/hooks/use-paywall';
21-
import { trySubmitSafe } from '@/utils/form';
6+
import { FormCardSkeleton } from '@/components/FormCard';
7+
import { type RequestError } from '@/hooks/use-api';
228

239
import PaywallNotification from '../PaywallNotification';
2410

25-
import CaptchaCard from './CaptchaCard';
26-
import CreateCaptchaForm from './CreateCaptchaForm';
27-
import EnableCaptcha from './EnableCaptcha';
11+
import CaptchaForm from './CaptchaForm';
2812
import Guide from './Guide';
2913
import styles from './index.module.scss';
3014
import useDataFetch from './use-data-fetch';
3115

3216
function Captcha() {
33-
const { guideId } = useParams();
34-
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' });
35-
const { isFreeTenant } = usePaywall();
36-
const { mutateSubscriptionQuotaAndUsages } = useContext(SubscriptionDataContext);
37-
38-
const [isCreateCaptchaFormOpen, setIsCreateCaptchaFormOpen] = useState(false);
39-
const { data, isLoading } = useDataFetch();
40-
const formMethods = useForm<CaptchaPolicy>({
41-
defaultValues: {
42-
enabled: false,
43-
},
44-
mode: 'onBlur',
45-
});
46-
const {
47-
reset,
48-
handleSubmit,
49-
formState: { isDirty, isSubmitting },
50-
} = formMethods;
51-
const api = useApi();
52-
53-
const onSubmit = trySubmitSafe(async (data: CaptchaPolicy) => {
54-
const { captchaPolicy } = await api
55-
.patch('api/sign-in-exp', {
56-
json: { captchaPolicy: data },
57-
})
58-
.json<SignInExperience>();
59-
reset(captchaPolicy);
60-
mutateSubscriptionQuotaAndUsages();
61-
toast.success(t('general.saved'));
62-
});
17+
const { data, isLoading: isDataLoading } = useDataFetch();
18+
const { data: signInExpData, isLoading: isSignInExpLoading } = useSWR<
19+
SignInExperience,
20+
RequestError
21+
>('api/sign-in-exp');
22+
const isLoading = isDataLoading || isSignInExpLoading;
6323

24+
const { guideId } = useParams();
6425
const guideType = z.nativeEnum(CaptchaType).safeParse(guideId);
6526
const navigate = useNavigate();
6627

@@ -78,59 +39,11 @@ function Captcha() {
7839
return (
7940
<div className={styles.content}>
8041
<PaywallNotification className={styles.upsellNotice} />
81-
{isLoading ? (
42+
{isLoading || !signInExpData ? (
8243
<FormCardSkeleton formFieldCount={2} />
8344
) : (
84-
<FormProvider {...formMethods}>
85-
<DetailsForm
86-
isDirty={isDirty}
87-
isSubmitting={isSubmitting}
88-
onSubmit={handleSubmit(onSubmit)}
89-
onDiscard={reset}
90-
>
91-
<FormCard
92-
title="security.bot_protection.title"
93-
description="security.bot_protection.description"
94-
learnMoreLink={{ href: captcha }}
95-
tag={
96-
<CombinedAddOnAndFeatureTag
97-
hasAddOnTag
98-
paywall={latestProPlanId}
99-
addOnLabel={addOnLabels.addOnBundle}
100-
/>
101-
}
102-
>
103-
<FormField title="security.bot_protection.captcha.title">
104-
<div className={styles.description}>
105-
{t('security.bot_protection.captcha.placeholder')}
106-
</div>
107-
{data && !isFreeTenant ? (
108-
<>
109-
<CaptchaCard captchaProvider={data} />
110-
<EnableCaptcha />
111-
</>
112-
) : (
113-
<Button
114-
title="security.bot_protection.captcha.add"
115-
icon={<Plus />}
116-
disabled={isFreeTenant}
117-
onClick={() => {
118-
setIsCreateCaptchaFormOpen(true);
119-
}}
120-
/>
121-
)}
122-
</FormField>
123-
</FormCard>
124-
</DetailsForm>
125-
</FormProvider>
45+
<CaptchaForm captchaProvider={data} formData={signInExpData.captchaPolicy} />
12646
)}
127-
<CreateCaptchaForm
128-
isOpen={isCreateCaptchaFormOpen}
129-
onClose={() => {
130-
setIsCreateCaptchaFormOpen(false);
131-
}}
132-
/>
133-
<UnsavedChangesAlertModal hasUnsavedChanges={isDirty} />
13447
</div>
13548
);
13649
}

0 commit comments

Comments
 (0)