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: {