Auth Email Change
Auth Email Change is a React and Tailwind CSS component from HextaUI, licensed MIT. Copy it and paste it into your project.
HextaUI
MIT
other
What it is
Auth Email Change is a React and Tailwind CSS component from HextaUI, licensed MIT. Copy it and paste it into your project.
How to use it
Open it in the catalogue, press copy, and paste it into your project. The prompt you copy carries the code and the rules that tell your agent to reproduce it without changing anything.
Five free copies a month. No card.
No packages beyond the project baseline
Licence
This component comes from HextaUI and is redistributed under the MIT licence, which permits commercial use. The copyright notice travels with the code.
Auth Email Change
"use client";
import { CheckCircle2, Eye, EyeOff, Loader2, Mail, Shield } from "lucide-react";
import { useCallback, useState } from "react";
import { cn } from "@/lib/utils";
import { Button } from "@/registry/new-york/ui/button";
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@/registry/new-york/ui/card";
import {
Field,
FieldContent,
FieldDescription,
FieldError,
FieldLabel,
} from "@/registry/new-york/ui/field";
import {
InputGroup,
InputGroupAddon,
InputGroupButton,
InputGroupInput,
} from "@/registry/new-york/ui/input-group";
export interface AuthEmailChangeProps {
currentEmail?: string;
onSubmit?: (data: { newEmail: string; password: string }) => void;
className?: string;
isLoading?: boolean;
isSuccess?: boolean;
errors?: {
newEmail?: string;
password?: string;
general?: string;
};
successMessage?: string;
}
interface FormErrors {
newEmail?: string;
password?: string;
}
function validateEmail(
value: string,
currentEmail?: string
): string | undefined {
if (!value.trim()) {
return "Email is required";
}
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (!emailRegex.test(value)) {
return "Please enter a valid email address";
}
if (value.trim().toLowerCase() === currentEmail?.toLowerCase()) {
return "New email must be different from current email";
}
return;
}
function validatePassword(value: string): string | undefined {
if (!value) {
return "Password is required";
}
return;
}
interface ErrorAlertProps {
message: string;
}
function ErrorAlert({ message }: ErrorAlertProps) {
return (
<div
aria-live="polite"
className="rounded-lg border border-destructive/50 bg-destructive/10 p-3 text-destructive text-sm"
role="alert"
>
{message}
</div>
);
}
interface CurrentEmailDisplayProps {
email: string;
}
function CurrentEmailDisplay({ email }: CurrentEmailDisplayProps) {
return (
<section
aria-label="Current email"
className="flex items-center gap-3 rounded-lg border border-muted bg-background px-4 py-3"
>
<span className="hidden size-12 items-center justify-center rounded-full bg-muted/70 sm:flex">
<Mail aria-hidden="true" className="size-4 text-muted-foreground" />
</span>
<div className="flex min-w-0 flex-col gap-0.5">
<span className="text-muted-foreground text-xs leading-none">
Signed in as
</span>
<span
className="truncate font-medium font-mono text-foreground text-sm"
title={email}
>
{email}
</span>
</div>
</section>
);
}
interface EmailFieldProps {
id: string;
value: string;
onChange: (e: React.ChangeEvent<HTMLInputElement>) => void;
error?: string;
}
function EmailField({ id, value, onChange, error }: EmailFieldProps) {
return (
<Field data-invalid={!!error}>
<FieldLabel htmlFor={id}>
New email address
<span aria-label="required" className="text-destructive">
*
</span>
</FieldLabel>
<FieldContent>
<InputGroup aria-invalid={!!error}>
<InputGroupAddon>
<Mail aria-hidden="true" className="size-4" />
</InputGroupAddon>
<InputGroupInput
aria-describedby={error ? `${id}-error` : undefined}
aria-invalid={!!error}
autoComplete="email"
id={id}
inputMode="email"
name="newEmail"
onChange={onChange}
placeholder="new@example.com…"
required
type="email"
value={value}
/>
</InputGroup>
{error && <FieldError id={`${id}-error`}>{error}</FieldError>}
<FieldDescription>
We'll send a verification link to this address
</FieldDescription>
</FieldContent>
</Field>
);
}
interface PasswordFieldProps {
id: string;
value: string;
onChange: (e: React.ChangeEvent<HTMLInputElement>) => void;
showPassword: boolean;
onTogglePassword: () => void;
error?: string;
}
function PasswordField({
id,
value,
onChange,
showPassword,
onTogglePassword,
error,
}: PasswordFieldProps) {
return (
<Field data-invalid={!!error}>
<FieldLabel htmlFor={id}>
Password
<span aria-label="required" className="text-destructive">
*
</span>
</FieldLabel>
<FieldContent>
<InputGroup aria-invalid={!!error}>
<InputGroupAddon>
<Shield aria-hidden="true" className="size-4" />
</InputGroupAddon>
<InputGroupInput
aria-describedby={error ? `${id}-error` : undefined}
aria-invalid={!!error}
autoComplete="current-password"
id={id}
name="password"
onChange={onChange}
placeholder="Enter your password…"
required
type={showPassword ? "text" : "password"}
value={value}
/>
<InputGroupButton
aria-label={showPassword ? "Hide password" : "Show password"}
className="min-h-[32px] min-w-[32px] touch-manipulation"
onClick={(e) => {
e.preventDefault();
onTogglePassword();
}}
type="button"
>
{showPassword ? (
<EyeOff aria-hidden="true" className="size-4" />
) : (
<Eye aria-hidden="true" className="size-4" />
)}
</InputGroupButton>
</InputGroup>
{error && <FieldError id={`${id}-error`}>{error}</FieldError>}
<FieldDescription>
Enter your current password to confirm this change
</FieldDescription>
</FieldContent>
</Field>
);
}
interface SuccessStateProps {
message: string;
newEmail?: string;
className?: string;
}
function SuccessState({ message, newEmail, className }: SuccessStateProps) {
return (
<Card className={cn("w-full max-w-sm shadow-xs", className)}>
<CardHeader>
<CardTitle>Verification email sent</CardTitle>
<CardDescription>
Check your new email address to verify the change
</CardDescription>
</CardHeader>
<CardContent>
<div className="flex flex-col items-center gap-4 rounded-lg border border-primary/20 bg-primary/5 p-6 text-center">
<div className="flex size-16 items-center justify-center rounded-full bg-primary/10">
<CheckCircle2 aria-hidden="true" className="size-8 text-primary" />
</div>
<div className="flex flex-col gap-2">
<p className="font-medium text-sm">{message}</p>
{newEmail && (
<p className="text-muted-foreground text-sm">
Sent to <span className="font-medium">{newEmail}</span>
</p>
)}
<p className="text-muted-foreground text-xs">
Click the verification link in the email to complete the change.
If you don't see it, check your spam folder.
</p>
</div>
</div>
</CardContent>
</Card>
);
}
export default function AuthEmailChange({
currentEmail,
onSubmit,
className,
isLoading = false,
isSuccess = false,
errors,
successMessage = "We've sent a verification link to your new email address.",
}: AuthEmailChangeProps) {
const [newEmail, setNewEmail] = useState("");
const [password, setPassword] = useState("");
const [showPassword, setShowPassword] = useState(false);
const [localErrors, setLocalErrors] = useState<FormErrors>({});
const handleSubmit = useCallback(
(e: React.FormEvent) => {
e.preventDefault();
const newEmailError = validateEmail(newEmail, currentEmail);
const passwordError = validatePassword(password);
if (newEmailError || passwordError) {
setLocalErrors({
newEmail: newEmailError,
password: passwordError,
});
return;
}
setLocalErrors({});
onSubmit?.({
newEmail: newEmail.trim(),
password,
});
},
[newEmail, password, currentEmail, onSubmit]
);
const handleNewEmailChange = useCallback(
(e: React.ChangeEvent<HTMLInputElement>) => {
const value = e.target.value;
setNewEmail(value);
if (localErrors.newEmail) {
setLocalErrors((prev) => ({
...prev,
newEmail: validateEmail(value, currentEmail),
}));
}
},
[localErrors.newEmail, currentEmail]
);
const handlePasswordChange = useCallback(
(e: React.ChangeEvent<HTMLInputElement>) => {
const value = e.target.value;
setPassword(value);
if (localErrors.password) {
setLocalErrors((prev) => ({
...prev,
password: validatePassword(value),
}));
}
},
[localErrors.password]
);
const handleTogglePassword = useCallback(() => {
setShowPassword((prev) => !prev);
}, []);
const newEmailError = errors?.newEmail || localErrors.newEmail;
const passwordError = errors?.password || localErrors.password;
const generalError = errors?.general;
if (isSuccess) {
return (
<SuccessState
className={className}
message={successMessage}
newEmail={newEmail}
/>
);
}
return (
<Card className={cn("w-full max-w-sm shadow-xs", className)}>
<CardHeader>
<CardTitle>Change email address</CardTitle>
<CardDescription>
Update your email address. We'll send a verification link to your
new email.
</CardDescription>
</CardHeader>
<CardContent>
<form className="flex flex-col gap-6" onSubmit={handleSubmit}>
{generalError && <ErrorAlert message={generalError} />}
{currentEmail && <CurrentEmailDisplay email={currentEmail} />}
<div className="flex flex-col gap-4">
<EmailField
error={newEmailError}
id="email-change-new"
onChange={handleNewEmailChange}
value={newEmail}
/>
<PasswordField
error={passwordError}
id="email-change-password"
onChange={handlePasswordChange}
onTogglePassword={handleTogglePassword}
showPassword={showPassword}
value={password}
/>
</div>
<Button
aria-busy={isLoading}
className="min-h-[44px] w-full touch-manipulation"
data-loading={isLoading}
disabled={isLoading}
type="submit"
>
{isLoading ? (
<>
<Loader2 aria-hidden="true" className="size-4 animate-spin" />
Sending verification email…
</>
) : (
<>
<Mail aria-hidden="true" className="size-4" />
Change email address
</>
)}
</Button>
</form>
</CardContent>
</Card>
);
}