fix(auth): use feat's clean LoginForm/SignupForm (getAppLogin via SocialButtons; password+PKCE) — auto-merge had mixed main's appLogin/sendLoginCode API with feat's client

This commit is contained in:
2026-06-21 19:43:15 -07:00
parent 7078723397
commit 20bf75282e
2 changed files with 53 additions and 207 deletions
+33 -166
View File
@@ -1,8 +1,6 @@
import { useEffect, useState, type FormEvent } from 'react'
import { useState, type FormEvent } from 'react'
import type { AuthClient } from '../client'
import type { AppLoginInfo, LoginResponse } from '../types'
import { ProviderButtons } from './ProviderButtons'
import { OTPForm } from './OTPForm'
import type { LoginResponse } from '../types'
export interface LoginFormProps {
readonly client: AuthClient
@@ -17,44 +15,12 @@ export interface LoginFormProps {
export function LoginForm(props: LoginFormProps) {
const { client } = props
const [app, setApp] = useState<AppLoginInfo | null>(null)
const [mode, setMode] = useState<'password' | 'code'>('password')
const [identifier, setIdentifier] = useState('')
const [password, setPassword] = useState('')
const [codeSent, setCodeSent] = useState(false)
const [busy, setBusy] = useState(false)
const [error, setError] = useState<string | null>(null)
const [notice, setNotice] = useState<string | null>(null)
// Load the application's providers + sign-in methods so we render exactly
// what IAM offers (social buttons, email/SMS code). Best-effort: on failure
// we still show password login.
useEffect(() => {
let alive = true
client
.appLogin()
.then((a) => {
if (alive) setApp(a)
})
.catch(() => {})
return () => {
alive = false
}
}, [client])
const codeEnabled =
!!app &&
(app.enableCodeSignin ||
app.signinMethods.some((m) => m.name === 'Verification code' && m.rule !== 'None'))
function handleResult(res: LoginResponse) {
if (res.error) setError(res.error)
else if (res.mfaRequired) props.onMfaRequired?.(res)
else if (res.redirectUrl) window.location.href = res.redirectUrl
else props.onSuccess?.(res)
}
async function onPasswordSubmit(e: FormEvent) {
async function onSubmit(e: FormEvent) {
e.preventDefault()
setBusy(true)
setError(null)
@@ -70,26 +36,14 @@ export function LoginForm(props: LoginFormProps) {
codeChallenge: props.codeChallenge,
codeChallengeMethod: props.codeChallengeMethod,
})
handleResult(res)
} catch (err) {
setError(String(err))
} finally {
setBusy(false)
}
}
async function onSendCode(e: FormEvent) {
e.preventDefault()
setBusy(true)
setError(null)
setNotice(null)
try {
const r = await client.sendLoginCode(identifier)
if (r.ok) {
setCodeSent(true)
setNotice(`Code sent to ${identifier}`)
if (res.error) {
setError(res.error)
} else if (res.mfaRequired) {
props.onMfaRequired?.(res)
} else if (res.redirectUrl) {
window.location.href = res.redirectUrl
} else {
setError(r.error ?? 'Could not send code')
props.onSuccess?.(res)
}
} catch (err) {
setError(String(err))
@@ -98,117 +52,30 @@ export function LoginForm(props: LoginFormProps) {
}
}
async function onVerifyCode(code: string) {
setBusy(true)
setError(null)
try {
const res = await client.loginWithCode({
dest: identifier,
code,
clientId: props.clientIdOverride ?? client.tenant.clientId,
application: client.tenant.appName,
organization: client.tenant.orgId,
redirectUri: props.redirectUri,
state: props.state,
})
handleResult(res)
} catch (err) {
setError(String(err))
} finally {
setBusy(false)
}
}
return (
<div className="hanzo-id-login">
{app && app.providers.length > 0 ? (
<ProviderButtons
client={client}
providers={app.providers}
mode="login"
redirectUri={props.redirectUri}
state={props.state}
clientIdOverride={props.clientIdOverride}
<form onSubmit={onSubmit} className="hanzo-id-login-form" aria-busy={busy}>
<label>
<span>Email or username</span>
<input
type="text"
autoComplete="username"
value={identifier}
onChange={(e) => setIdentifier(e.target.value)}
required
/>
) : null}
{mode === 'password' ? (
<form onSubmit={onPasswordSubmit} className="hanzo-id-login-form" aria-busy={busy}>
<label>
<span>Email or username</span>
<input
type="text"
autoComplete="username"
value={identifier}
onChange={(e) => setIdentifier(e.target.value)}
required
/>
</label>
<label>
<span>Password</span>
<input
type="password"
autoComplete="current-password"
value={password}
onChange={(e) => setPassword(e.target.value)}
required
/>
</label>
{error ? <p role="alert" className="hanzo-id-error">{error}</p> : null}
<button type="submit" disabled={busy}>{busy ? 'Signing in…' : 'Sign in'}</button>
</form>
) : (
<div className="hanzo-id-code-login">
{!codeSent ? (
<form onSubmit={onSendCode} className="hanzo-id-login-form" aria-busy={busy}>
<label>
<span>Email or phone</span>
<input
type="text"
autoComplete="username"
placeholder="[email protected] or +1 555 555 5555"
value={identifier}
onChange={(e) => setIdentifier(e.target.value)}
required
/>
</label>
{error ? <p role="alert" className="hanzo-id-error">{error}</p> : null}
<button type="submit" disabled={busy}>{busy ? 'Sending…' : 'Send code'}</button>
</form>
) : (
<>
{notice ? <p className="hanzo-id-notice">{notice}</p> : null}
<OTPForm channel={identifier.includes('@') ? 'email' : 'sms'} onSubmit={onVerifyCode} />
{error ? <p role="alert" className="hanzo-id-error">{error}</p> : null}
<button
type="button"
className="hanzo-id-linkbtn"
onClick={() => {
setCodeSent(false)
setNotice(null)
}}
>
Use a different address
</button>
</>
)}
</div>
)}
{codeEnabled ? (
<button
type="button"
className="hanzo-id-linkbtn hanzo-id-toggle-mode"
onClick={() => {
setMode(mode === 'password' ? 'code' : 'password')
setError(null)
setNotice(null)
setCodeSent(false)
}}
>
{mode === 'password' ? 'Sign in with email or SMS code' : 'Sign in with password'}
</button>
) : null}
</div>
</label>
<label>
<span>Password</span>
<input
type="password"
autoComplete="current-password"
value={password}
onChange={(e) => setPassword(e.target.value)}
required
/>
</label>
{error ? <p role="alert" className="hanzo-id-error">{error}</p> : null}
<button type="submit" disabled={busy}>{busy ? 'Signing in…' : 'Sign in'}</button>
</form>
)
}
+20 -41
View File
@@ -1,7 +1,5 @@
import { useEffect, useState, type FormEvent } from 'react'
import { useState, type FormEvent } from 'react'
import type { AuthClient } from '../client'
import type { AppLoginInfo } from '../types'
import { ProviderButtons } from './ProviderButtons'
export interface SignupFormProps {
readonly client: AuthClient
@@ -11,25 +9,11 @@ export interface SignupFormProps {
export function SignupForm(props: SignupFormProps) {
const { client } = props
const [app, setApp] = useState<AppLoginInfo | null>(null)
const [email, setEmail] = useState('')
const [password, setPassword] = useState('')
const [busy, setBusy] = useState(false)
const [error, setError] = useState<string | null>(null)
useEffect(() => {
let alive = true
client
.appLogin()
.then((a) => {
if (alive) setApp(a)
})
.catch(() => {})
return () => {
alive = false
}
}, [client])
async function onSubmit(e: FormEvent) {
e.preventDefault()
setBusy(true)
@@ -54,29 +38,24 @@ export function SignupForm(props: SignupFormProps) {
}
return (
<div className="hanzo-id-signup">
{app && app.providers.length > 0 ? (
<ProviderButtons client={client} providers={app.providers} mode="signup" />
) : null}
<form onSubmit={onSubmit} className="hanzo-id-signup-form" aria-busy={busy}>
<label>
<span>Email</span>
<input type="email" autoComplete="email" value={email} onChange={(e) => setEmail(e.target.value)} required />
</label>
<label>
<span>Password</span>
<input
type="password"
autoComplete="new-password"
value={password}
onChange={(e) => setPassword(e.target.value)}
minLength={12}
required
/>
</label>
{error ? <p role="alert" className="hanzo-id-error">{error}</p> : null}
<button type="submit" disabled={busy}>{busy ? 'Creating account…' : 'Create account'}</button>
</form>
</div>
<form onSubmit={onSubmit} className="hanzo-id-signup-form" aria-busy={busy}>
<label>
<span>Email</span>
<input type="email" autoComplete="email" value={email} onChange={(e) => setEmail(e.target.value)} required />
</label>
<label>
<span>Password</span>
<input
type="password"
autoComplete="new-password"
value={password}
onChange={(e) => setPassword(e.target.value)}
minLength={12}
required
/>
</label>
{error ? <p role="alert" className="hanzo-id-error">{error}</p> : null}
<button type="submit" disabled={busy}>{busy ? 'Creating account…' : 'Create account'}</button>
</form>
)
}