fix: Registration Form Legal Checkbox Logic (#10597)

Closes #10498

The registration form's legal checkboxes had incorrect validation logic
that prevented users from completing registration when only one legal
document (ToS or Privacy Policy) was configured, or when no legal
documents were required.

additionally removes a duplicate description for "or use Identity
Provider"

# Which Problems Are Solved

Having only partial legal documents was blocking users to register. The
logic now conditionally renders checkboxes and checks if all provided
documents are accepted.

# How the Problems Are Solved

- Fixed checkbox validation: Now properly validates based on which legal
documents are actually available
- acceptance logic: Only requires acceptance of checkboxes that are
shown
- No legal docs support: Users can proceed when no legal documents are
configured
- Proper state management: Fixed checkbox state tracking and mixed-up
test IDs

---------

Co-authored-by: Stefan Benz <[email protected]>
This commit is contained in:
Max Peintner
2025-09-09 07:37:32 +00:00
committed by GitHub
co-authored by Stefan Benz
parent a0c3ccecf7
commit b9b9baf67f
11 changed files with 2597 additions and 2631 deletions
@@ -21,6 +21,18 @@ export function PrivacyPolicyCheckboxes({ legal, onChange }: Props) {
privacyPolicyAccepted: false,
});
// Helper function to check if all required checkboxes are accepted
const checkAllAccepted = (newState: AcceptanceState) => {
const hasTosLink = !!legal?.tosLink;
const hasPrivacyLink = !!legal?.privacyPolicyLink;
// Check that all required checkboxes are accepted
return (
(!hasTosLink || newState.tosAccepted) &&
(!hasPrivacyLink || newState.privacyPolicyAccepted)
);
};
return (
<>
<p className="mt-4 flex flex-row items-center text-sm text-text-light-secondary-500 dark:text-text-dark-secondary-500">
@@ -50,16 +62,17 @@ export function PrivacyPolicyCheckboxes({ legal, onChange }: Props) {
<div className="mt-4 flex items-center">
<Checkbox
className="mr-4"
checked={false}
value={"privacypolicy"}
checked={acceptanceState.tosAccepted}
value={"tos"}
onChangeVal={(checked: boolean) => {
setAcceptanceState({
const newState = {
...acceptanceState,
tosAccepted: checked,
});
onChange(checked && acceptanceState.privacyPolicyAccepted);
};
setAcceptanceState(newState);
onChange(checkAllAccepted(newState));
}}
data-testid="privacy-policy-checkbox"
data-testid="tos-checkbox"
/>
<div className="mr-4 w-[28rem]">
@@ -75,25 +88,22 @@ export function PrivacyPolicyCheckboxes({ legal, onChange }: Props) {
<div className="mt-4 flex items-center">
<Checkbox
className="mr-4"
checked={false}
value={"tos"}
checked={acceptanceState.privacyPolicyAccepted}
value={"privacypolicy"}
onChangeVal={(checked: boolean) => {
setAcceptanceState({
const newState = {
...acceptanceState,
privacyPolicyAccepted: checked,
});
onChange(checked && acceptanceState.tosAccepted);
};
setAcceptanceState(newState);
onChange(checkAllAccepted(newState));
}}
data-testid="tos-checkbox"
data-testid="privacy-policy-checkbox"
/>
<div className="mr-4 w-[28rem]">
<p className="text-sm text-text-light-500 dark:text-text-dark-500">
<Link
href={legal.privacyPolicyLink}
className="underline"
target="_blank"
>
<Link href={legal.privacyPolicyLink} className="underline" target="_blank">
<Translated i18nKey="privacyPolicy" namespace="register" />
</Link>
</p>
+24 -43
View File
@@ -2,20 +2,13 @@
import { registerUser } from "@/lib/server/register";
import { LegalAndSupportSettings } from "@zitadel/proto/zitadel/settings/v2/legal_settings_pb";
import {
LoginSettings,
PasskeysType,
} from "@zitadel/proto/zitadel/settings/v2/login_settings_pb";
import { LoginSettings, PasskeysType } from "@zitadel/proto/zitadel/settings/v2/login_settings_pb";
import { useRouter } from "next/navigation";
import { useState } from "react";
import { useTranslations } from "next-intl";
import { FieldValues, useForm } from "react-hook-form";
import { Alert, AlertType } from "./alert";
import {
AuthenticationMethod,
AuthenticationMethodRadio,
methods,
} from "./authentication-method-radio";
import { AuthenticationMethod, AuthenticationMethodRadio, methods } from "./authentication-method-radio";
import { BackButton } from "./back-button";
import { Button, ButtonVariants } from "./button";
import { TextInput } from "./input";
@@ -98,10 +91,7 @@ export function RegisterForm({
return response;
}
async function submitAndContinue(
value: Inputs,
withPassword: boolean = false,
) {
async function submitAndContinue(value: Inputs, withPassword: boolean = false) {
const registerParams: any = value;
if (organization) {
@@ -114,9 +104,7 @@ export function RegisterForm({
// redirect user to /register/password if password is chosen
if (withPassword) {
return router.push(
`/register/password?` + new URLSearchParams(registerParams),
);
return router.push(`/register/password?` + new URLSearchParams(registerParams));
} else {
return submitAndRegister(value);
}
@@ -125,6 +113,11 @@ export function RegisterForm({
const { errors } = formState;
const [tosAndPolicyAccepted, setTosAndPolicyAccepted] = useState(false);
// Check if legal acceptance is required
const isLegalAcceptanceRequired = !!(legal?.tosLink || legal?.privacyPolicyLink);
const canSubmit = formState.isValid && (!isLegalAcceptanceRequired || tosAndPolicyAccepted);
return (
<form className="w-full">
<div className="mb-4 grid grid-cols-2 gap-4">
@@ -162,38 +155,27 @@ export function RegisterForm({
/>
</div>
</div>
{legal && (
<PrivacyPolicyCheckboxes
legal={legal}
onChange={setTosAndPolicyAccepted}
/>
{(legal?.tosLink || legal?.privacyPolicyLink) && (
<PrivacyPolicyCheckboxes legal={legal} onChange={setTosAndPolicyAccepted} />
)}
{/* show chooser if both methods are allowed */}
{loginSettings &&
loginSettings.allowUsernamePassword &&
loginSettings.passkeysType == PasskeysType.ALLOWED && (
<>
<p className="ztdl-p mb-6 mt-4 block text-left">
<Translated i18nKey="selectMethod" namespace="register" />
</p>
{loginSettings && loginSettings.allowUsernamePassword && loginSettings.passkeysType == PasskeysType.ALLOWED && (
<>
<p className="ztdl-p mb-6 mt-4 block text-left">
<Translated i18nKey="selectMethod" namespace="register" />
</p>
<div className="pb-4">
<AuthenticationMethodRadio
selected={selected}
selectionChanged={setSelected}
/>
</div>
</>
)}
<div className="pb-4">
<AuthenticationMethodRadio selected={selected} selectionChanged={setSelected} />
</div>
</>
)}
{!loginSettings?.allowUsernamePassword &&
loginSettings?.passkeysType !== PasskeysType.ALLOWED &&
(!loginSettings?.allowExternalIdp || !idpCount) && (
<div className="py-4">
<Alert type={AlertType.INFO}>
<Translated
i18nKey="noMethodAvailableWarning"
namespace="register"
/>
<Translated i18nKey="noMethodAvailableWarning" namespace="register" />
</Alert>
</div>
)}
@@ -209,11 +191,10 @@ export function RegisterForm({
<Button
type="submit"
variant={ButtonVariants.Primary}
disabled={loading || !formState.isValid || !tosAndPolicyAccepted}
disabled={loading || !canSubmit}
onClick={handleSubmit((values) => {
const usePasswordToContinue: boolean =
loginSettings?.allowUsernamePassword &&
loginSettings?.passkeysType == PasskeysType.ALLOWED
loginSettings?.allowUsernamePassword && loginSettings?.passkeysType == PasskeysType.ALLOWED
? !(selected === methods[0]) // choose selection if both available
: !!loginSettings?.allowUsernamePassword; // if password is chosen
// set password as default if only password is allowed