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>
This commit is contained in:
@@ -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