mirror of
https://github.com/zitadel/zitadel.git
synced 2025-11-01 00:46:23 +00:00
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 <46600784+stebenz@users.noreply.github.com>
(cherry picked from commit b9b9baf67f)
This commit is contained in:
committed by
Livio Spring
parent
0f6380b474
commit
462e266604
@@ -222,7 +222,6 @@
|
||||
"termsOfService": "Nutzungsbedingungen",
|
||||
"privacyPolicy": "Datenschutzrichtlinie",
|
||||
"submit": "Weiter",
|
||||
"orUseIDP": "oder verwenden Sie einen Identitätsanbieter",
|
||||
"password": {
|
||||
"title": "Passwort festlegen",
|
||||
"description": "Legen Sie das Passwort für Ihr Konto fest",
|
||||
|
||||
@@ -222,7 +222,6 @@
|
||||
"termsOfService": "Terms of Service",
|
||||
"privacyPolicy": "Privacy Policy",
|
||||
"submit": "Continue",
|
||||
"orUseIDP": "or use an Identity Provider",
|
||||
"password": {
|
||||
"title": "Set Password",
|
||||
"description": "Set the password for your account",
|
||||
|
||||
@@ -222,7 +222,6 @@
|
||||
"termsOfService": "Términos de Servicio",
|
||||
"privacyPolicy": "Política de Privacidad",
|
||||
"submit": "Continuar",
|
||||
"orUseIDP": "o usa un Proveedor de Identidad",
|
||||
"password": {
|
||||
"title": "Establecer Contraseña",
|
||||
"description": "Establece la contraseña para tu cuenta",
|
||||
|
||||
@@ -222,7 +222,6 @@
|
||||
"termsOfService": "Termini di Servizio",
|
||||
"privacyPolicy": "Informativa sulla Privacy",
|
||||
"submit": "Continua",
|
||||
"orUseIDP": "o usa un Identity Provider",
|
||||
"password": {
|
||||
"title": "Imposta Password",
|
||||
"description": "Imposta la password per il tuo account",
|
||||
|
||||
@@ -222,7 +222,6 @@
|
||||
"termsOfService": "Regulamin",
|
||||
"privacyPolicy": "Polityka prywatności",
|
||||
"submit": "Kontynuuj",
|
||||
"orUseIDP": "lub użyj dostawcy tożsamości",
|
||||
"password": {
|
||||
"title": "Ustaw hasło",
|
||||
"description": "Ustaw hasło dla swojego konta",
|
||||
|
||||
@@ -222,7 +222,6 @@
|
||||
"termsOfService": "Условия использования",
|
||||
"privacyPolicy": "Политика конфиденциальности",
|
||||
"submit": "Продолжить",
|
||||
"orUseIDP": "или используйте Identity Provider",
|
||||
"password": {
|
||||
"title": "Установить пароль",
|
||||
"description": "Установите пароль для вашего аккаунта",
|
||||
|
||||
@@ -222,7 +222,6 @@
|
||||
"termsOfService": "服务条款",
|
||||
"privacyPolicy": "隐私政策",
|
||||
"submit": "继续",
|
||||
"orUseIDP": "或使用身份提供者",
|
||||
"password": {
|
||||
"title": "设置密码",
|
||||
"description": "为您的账户设置密码",
|
||||
|
||||
@@ -20,12 +20,10 @@ import { headers } from "next/headers";
|
||||
|
||||
export async function generateMetadata(): Promise<Metadata> {
|
||||
const t = await getTranslations("register");
|
||||
return { title: t('title')};
|
||||
return { title: t("title") };
|
||||
}
|
||||
|
||||
export default async function Page(props: {
|
||||
searchParams: Promise<Record<string | number | symbol, string | undefined>>;
|
||||
}) {
|
||||
export default async function Page(props: { searchParams: Promise<Record<string | number | symbol, string | undefined>> }) {
|
||||
const searchParams = await props.searchParams;
|
||||
|
||||
let { firstname, lastname, email, organization, requestId } = searchParams;
|
||||
@@ -104,12 +102,9 @@ export default async function Page(props: {
|
||||
{legal &&
|
||||
passwordComplexitySettings &&
|
||||
organization &&
|
||||
(loginSettings.allowUsernamePassword ||
|
||||
loginSettings.passkeysType == PasskeysType.ALLOWED) && (
|
||||
(loginSettings.allowUsernamePassword || loginSettings.passkeysType == PasskeysType.ALLOWED) && (
|
||||
<RegisterForm
|
||||
idpCount={
|
||||
!loginSettings?.allowExternalIdp ? 0 : identityProviders.length
|
||||
}
|
||||
idpCount={!loginSettings?.allowExternalIdp ? 0 : identityProviders.length}
|
||||
legal={legal}
|
||||
organization={organization}
|
||||
firstname={firstname}
|
||||
@@ -122,12 +117,6 @@ export default async function Page(props: {
|
||||
|
||||
{loginSettings?.allowExternalIdp && !!identityProviders.length && (
|
||||
<>
|
||||
<div className="flex flex-col items-center py-3">
|
||||
<p className="ztdl-p text-center">
|
||||
<Translated i18nKey="orUseIDP" namespace="register" />
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<SignInWithIdp
|
||||
identityProviders={identityProviders}
|
||||
requestId={requestId}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user