diff --git a/apps/hosted-components/src/hosted-components/auth/auth-page.tsx b/apps/hosted-components/src/hosted-components/auth/auth-page.tsx index 3a5fbb987..280351658 100644 --- a/apps/hosted-components/src/hosted-components/auth/auth-page.tsx +++ b/apps/hosted-components/src/hosted-components/auth/auth-page.tsx @@ -92,6 +92,11 @@ function HostedAuthPageInner(props: { const projectFromHook = app.useProject(); const project: AuthProject = props.mockProject ?? projectFromHook; + // Lifted so the typed email survives switching between the "Email" (magic link) and + // "Email & Password" (credential) tabs — the tab content is unmounted when inactive, + // so keeping the email here (instead of inside each form) both persists and shares it. + const [email, setEmail] = useState(""); + if (props.automaticRedirect && user != null && props.mockProject == null) { return ( }> @@ -165,16 +170,16 @@ function HostedAuthPageInner(props: { Email & Password - + - {props.type === "sign-up" ? : } + {props.type === "sign-up" ? : } ) : project.config.credentialEnabled ? ( - props.type === "sign-up" ? : + props.type === "sign-up" ? : ) : project.config.magicLinkEnabled ? ( - + ) : !(hasOAuthProviders || hasPasskey) ? (

No authentication method enabled.

) : null} diff --git a/apps/hosted-components/src/hosted-components/auth/forms/credential-sign-in.tsx b/apps/hosted-components/src/hosted-components/auth/forms/credential-sign-in.tsx index 04f4b79c5..06fd0587d 100644 --- a/apps/hosted-components/src/hosted-components/auth/forms/credential-sign-in.tsx +++ b/apps/hosted-components/src/hosted-components/auth/forms/credential-sign-in.tsx @@ -7,9 +7,13 @@ import { Button, Input, Label, PasswordInput } from "~/components/ui"; import { FormWarningText } from "../supporting/form-elements"; import { isValidEmail } from "../supporting/utils"; -export function CredentialSignIn() { +export function CredentialSignIn(props: { + email: string, + onEmailChange: (email: string) => void, +}) { const app = useStackApp(); - const [email, setEmail] = useState(""); + const email = props.email; + const setEmail = props.onEmailChange; const [password, setPassword] = useState(""); const [emailError, setEmailError] = useState(null); const [passwordError, setPasswordError] = useState(null); @@ -52,6 +56,7 @@ export function CredentialSignIn() { id="email" type="email" autoComplete="email" + autoFocus className="h-10 rounded-xl border-border bg-background" value={email} onChange={(event) => { diff --git a/apps/hosted-components/src/hosted-components/auth/forms/credential-sign-up.tsx b/apps/hosted-components/src/hosted-components/auth/forms/credential-sign-up.tsx index efdf65388..7dabf52cb 100644 --- a/apps/hosted-components/src/hosted-components/auth/forms/credential-sign-up.tsx +++ b/apps/hosted-components/src/hosted-components/auth/forms/credential-sign-up.tsx @@ -10,9 +10,12 @@ import { isValidEmail } from "../supporting/utils"; export function CredentialSignUp(props: { noPasswordRepeat?: boolean, + email: string, + onEmailChange: (email: string) => void, }) { const app = useStackApp(); - const [email, setEmail] = useState(""); + const email = props.email; + const setEmail = props.onEmailChange; const [password, setPassword] = useState(""); const [passwordRepeat, setPasswordRepeat] = useState(""); const [emailError, setEmailError] = useState(null); @@ -67,6 +70,7 @@ export function CredentialSignUp(props: { id="email" type="email" autoComplete="email" + autoFocus className="h-10 rounded-xl border-border bg-background" value={email} onChange={(event) => { diff --git a/apps/hosted-components/src/hosted-components/auth/forms/magic-link-sign-in.tsx b/apps/hosted-components/src/hosted-components/auth/forms/magic-link-sign-in.tsx index 9f03bdfa4..600754ebe 100644 --- a/apps/hosted-components/src/hosted-components/auth/forms/magic-link-sign-in.tsx +++ b/apps/hosted-components/src/hosted-components/auth/forms/magic-link-sign-in.tsx @@ -1,7 +1,7 @@ import { KnownErrors } from "@hexclave/shared"; import { useStackApp } from "@hexclave/react"; import { runAsynchronouslyWithAlert } from "@hexclave/shared/dist/utils/promises"; -import { useEffect, useState } from "react"; +import { useEffect, useRef, useState } from "react"; import { Button, @@ -24,6 +24,13 @@ function MagicLinkOtp(props: { const [otp, setOtp] = useState(""); const [submitting, setSubmitting] = useState(false); const [error, setError] = useState(null); + const otpInputRef = useRef(null); + + // Focus the (visually hidden) OTP input on mount so the user can type or paste the + // code immediately without having to click on the boxes first. + useEffect(() => { + otpInputRef.current?.focus(); + }, []); useEffect(() => { if (otp.length !== 6 || submitting) { @@ -56,6 +63,7 @@ function MagicLinkOtp(props: {
Enter the code from your email void, +}) { const app = useStackApp(); - const [email, setEmail] = useState(""); + const email = props.email; + const setEmail = props.onEmailChange; const [emailError, setEmailError] = useState(null); const [nonce, setNonce] = useState(null); const [loading, setLoading] = useState(false); @@ -130,6 +142,7 @@ export function MagicLinkSignIn() { id="email" type="email" autoComplete="email" + autoFocus className="h-10 rounded-xl border-border bg-background" value={email} onChange={(event) => {