mirror of
https://github.com/andwati/zitadel.git
synced 2026-11-02 20:32:19 +00:00
<!-- Please inform yourself about the contribution guidelines on submitting a PR here: https://github.com/zitadel/zitadel/blob/main/CONTRIBUTING.md#submit-a-pull-request-pr. Take note of how PR/commit titles should be written and replace the template texts in the sections below. Don't remove any of the sections. It is important that the commit history clearly shows what is changed and why. Important: By submitting a contribution you agree to the terms from our Licensing Policy as described here: https://github.com/zitadel/zitadel/blob/main/LICENSING.md#community-contributions. --> # Which Problems Are Solved The first input field on login screens is not automatically focused, requiring users to manually click before typing. This creates unnecessary friction during the login flow, particularly for keyboard-oriented users and accessibility scenarios. # How the Problems Are Solved - Added the `autoFocus` prop to the first `TextInput` in all 12 login form components. - For forms with conditionally rendered first inputs (`set-password-form` and `register-form-idp-incomplete`), `autoFocus` is set dynamically so the correct field receives focus depending on which input is visible. # Additional Changes None # Additional Context - Closes #11565 Co-authored-by: Max Peintner <[email protected]>
211 lines
6.9 KiB
TypeScript
211 lines
6.9 KiB
TypeScript
"use client";
|
|
|
|
import { registerUser } from "@/lib/server/register";
|
|
import { handleServerActionResponse } from "@/lib/client";
|
|
import { LegalAndSupportSettings } from "@zitadel/proto/zitadel/settings/v2/legal_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 { BackButton } from "./back-button";
|
|
import { Button, ButtonVariants } from "./button";
|
|
import { TextInput } from "./input";
|
|
import { PrivacyPolicyCheckboxes } from "./privacy-policy-checkboxes";
|
|
import { Spinner } from "./spinner";
|
|
import { Translated } from "./translated";
|
|
import { AutoSubmitForm } from "./auto-submit-form";
|
|
|
|
type Inputs =
|
|
| {
|
|
firstname: string;
|
|
lastname: string;
|
|
email: string;
|
|
}
|
|
| FieldValues;
|
|
|
|
type Props = {
|
|
legal: LegalAndSupportSettings;
|
|
firstname?: string;
|
|
lastname?: string;
|
|
email?: string;
|
|
organization: string;
|
|
requestId?: string;
|
|
loginSettings?: LoginSettings;
|
|
idpCount: number;
|
|
};
|
|
|
|
export function RegisterForm({
|
|
legal,
|
|
email,
|
|
firstname,
|
|
lastname,
|
|
organization,
|
|
requestId,
|
|
loginSettings,
|
|
idpCount = 0,
|
|
}: Props) {
|
|
const { register, handleSubmit, formState } = useForm<Inputs>({
|
|
mode: "onChange",
|
|
defaultValues: {
|
|
email: email ?? "",
|
|
firstName: firstname ?? "",
|
|
lastname: lastname ?? "",
|
|
},
|
|
});
|
|
|
|
const t = useTranslations("register");
|
|
|
|
const [loading, setLoading] = useState<boolean>(false);
|
|
const [selected, setSelected] = useState<AuthenticationMethod>(methods[0]);
|
|
const [error, setError] = useState<string>("");
|
|
const [samlData, setSamlData] = useState<{ url: string; fields: Record<string, string> } | null>(null);
|
|
|
|
const router = useRouter();
|
|
|
|
async function submitAndRegister(values: Inputs) {
|
|
setLoading(true);
|
|
try {
|
|
const response = await registerUser({
|
|
email: values.email,
|
|
firstName: values.firstname,
|
|
lastName: values.lastname,
|
|
organization: organization,
|
|
requestId: requestId,
|
|
});
|
|
|
|
handleServerActionResponse(response, router, setSamlData, setError);
|
|
|
|
return response;
|
|
} catch {
|
|
setError(t("errors.couldNotRegisterUser"));
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
}
|
|
|
|
async function submitAndContinue(value: Inputs, withPassword: boolean = false) {
|
|
const registerParams: any = value;
|
|
|
|
if (organization) {
|
|
registerParams.organization = organization;
|
|
}
|
|
|
|
if (requestId) {
|
|
registerParams.requestId = requestId;
|
|
}
|
|
|
|
// redirect user to /register/password if password is chosen
|
|
if (withPassword) {
|
|
return router.push(`/register/password?` + new URLSearchParams(registerParams));
|
|
} else {
|
|
return submitAndRegister(value);
|
|
}
|
|
}
|
|
|
|
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 (
|
|
<>
|
|
{samlData && <AutoSubmitForm url={samlData.url} fields={samlData.fields} />}
|
|
<form className="w-full">
|
|
<div className="mb-4 grid grid-cols-2 gap-4">
|
|
<div className="">
|
|
<TextInput
|
|
type="firstname"
|
|
autoComplete="firstname"
|
|
autoFocus
|
|
required
|
|
{...register("firstname", { required: t("required.firstname") })}
|
|
label={t("labels.firstname")}
|
|
error={errors.firstname?.message as string}
|
|
data-testid="firstname-text-input"
|
|
/>
|
|
</div>
|
|
<div className="">
|
|
<TextInput
|
|
type="lastname"
|
|
autoComplete="lastname"
|
|
required
|
|
{...register("lastname", { required: t("required.lastname") })}
|
|
label={t("labels.lastname")}
|
|
error={errors.lastname?.message as string}
|
|
data-testid="lastname-text-input"
|
|
/>
|
|
</div>
|
|
<div className="col-span-2">
|
|
<TextInput
|
|
type="email"
|
|
autoComplete="email"
|
|
required
|
|
{...register("email", { required: t("required.email") })}
|
|
label={t("labels.email")}
|
|
error={errors.email?.message as string}
|
|
data-testid="email-text-input"
|
|
/>
|
|
</div>
|
|
</div>
|
|
{(legal?.tosLink || legal?.privacyPolicyLink) && (
|
|
<PrivacyPolicyCheckboxes legal={legal} onChange={setTosAndPolicyAccepted} />
|
|
)}
|
|
{/* show chooser if both methods are allowed */}
|
|
{loginSettings && loginSettings.allowLocalAuthentication && 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>
|
|
</>
|
|
)}
|
|
{!loginSettings?.allowLocalAuthentication &&
|
|
loginSettings?.passkeysType !== PasskeysType.ALLOWED &&
|
|
(!loginSettings?.allowExternalIdp || !idpCount) && (
|
|
<div className="py-4">
|
|
<Alert type={AlertType.INFO}>
|
|
<Translated i18nKey="noMethodAvailableWarning" namespace="register" />
|
|
</Alert>
|
|
</div>
|
|
)}
|
|
|
|
{error && (
|
|
<div className="py-4">
|
|
<Alert>{error}</Alert>
|
|
</div>
|
|
)}
|
|
|
|
<div className="mt-8 flex w-full flex-row items-center justify-between">
|
|
<BackButton data-testid="back-button" />
|
|
<Button
|
|
type="submit"
|
|
variant={ButtonVariants.Primary}
|
|
disabled={loading || !canSubmit}
|
|
onClick={handleSubmit((values) => {
|
|
const usePasswordToContinue: boolean =
|
|
loginSettings?.allowLocalAuthentication && loginSettings?.passkeysType == PasskeysType.ALLOWED
|
|
? !(selected === methods[0]) // choose selection if both available
|
|
: !!loginSettings?.allowLocalAuthentication; // if password is chosen
|
|
// set password as default if only password is allowed
|
|
return submitAndContinue(values, usePasswordToContinue);
|
|
})}
|
|
data-testid="submit-button"
|
|
>
|
|
{loading && <Spinner className="mr-2 h-5 w-5" />}
|
|
<Translated i18nKey="submit" namespace="register" />
|
|
</Button>
|
|
</div>
|
|
</form>
|
|
</>
|
|
);
|
|
}
|