Complete guide to building custom sign-in flows with validation, error handling, and multiple authentication strategies
import { useSignin, useNavigation } from '@snipextt/wacht'
import { useState } from 'react'
function BasicSignInForm() {
const [email, setEmail] = useState('')
const [password, setPassword] = useState('')
const { signIn, isLoading, error, clearError } = useSignin()
const { navigate } = useNavigation()
const handleSubmit = async (e) => {
e.preventDefault()
clearError()
try {
await signIn({
strategy: 'plain_email',
email,
password
})
// Success - redirect to dashboard
navigate('/dashboard')
} catch (err) {
// Error is automatically handled by the hook
console.error('Sign-in failed:', err)
}
}
return (
<form onSubmit={handleSubmit} className="signin-form">
<h2>Sign In</h2>
{error && (
<div className="error-banner">
<span>{error.message}</span>
<button type="button" onClick={clearError}>×</button>
</div>
)}
<div className="form-group">
<label htmlFor="email">Email</label>
<input
id="email"
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
placeholder="Enter your email"
required
/>
</div>
<div className="form-group">
<label htmlFor="password">Password</label>
<input
id="password"
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
placeholder="Enter your password"
required
/>
</div>
<button type="submit" disabled={isLoading}>
{isLoading ? 'Signing in...' : 'Sign In'}
</button>
</form>
)
}
import { useSignin, useNavigation } from '@snipextt/wacht'
import { useState, useEffect } from 'react'
function EnhancedSignInForm() {
const [formData, setFormData] = useState({
email: '',
password: ''
})
const [formErrors, setFormErrors] = useState({})
const [showPassword, setShowPassword] = useState(false)
const { signIn, isLoading, error, clearError } = useSignin()
const { navigate } = useNavigation()
// Clear server errors when user starts typing
useEffect(() => {
if (error && (formData.email || formData.password)) {
clearError()
}
}, [formData.email, formData.password, error, clearError])
const validateForm = () => {
const errors = {}
if (!formData.email) {
errors.email = 'Email is required'
} else if (!/\S+@\S+\.\S+/.test(formData.email)) {
errors.email = 'Please enter a valid email'
}
if (!formData.password) {
errors.password = 'Password is required'
} else if (formData.password.length < 6) {
errors.password = 'Password must be at least 6 characters'
}
setFormErrors(errors)
return Object.keys(errors).length === 0
}
const handleInputChange = (field) => (e) => {
setFormData(prev => ({
...prev,
[field]: e.target.value
}))
// Clear field error when user starts typing
if (formErrors[field]) {
setFormErrors(prev => ({
...prev,
[field]: ''
}))
}
}
const handleSubmit = async (e) => {
e.preventDefault()
if (!validateForm()) return
try {
await signIn({
strategy: 'plain_email',
email: formData.email,
password: formData.password
})
navigate('/dashboard')
} catch (err) {
console.error('Sign-in failed:', err)
}
}
return (
<div className="signin-container">
<form onSubmit={handleSubmit} className="signin-form">
<div className="form-header">
<h2>Welcome Back</h2>
<p>Sign in to your account</p>
</div>
{error && (
<div className="error-banner">
<div className="error-content">
<span className="error-icon">⚠️</span>
<span>{error.message}</span>
</div>
<button type="button" onClick={clearError}>×</button>
</div>
)}
<div className="form-group">
<label htmlFor="email">Email Address</label>
<input
id="email"
type="email"
value={formData.email}
onChange={handleInputChange('email')}
placeholder="Enter your email"
className={formErrors.email ? 'error' : ''}
aria-invalid={!!formErrors.email}
aria-describedby={formErrors.email ? 'email-error' : undefined}
/>
{formErrors.email && (
<span id="email-error" className="field-error">
{formErrors.email}
</span>
)}
</div>
<div className="form-group">
<label htmlFor="password">Password</label>
<div className="password-input">
<input
id="password"
type={showPassword ? 'text' : 'password'}
value={formData.password}
onChange={handleInputChange('password')}
placeholder="Enter your password"
className={formErrors.password ? 'error' : ''}
aria-invalid={!!formErrors.password}
aria-describedby={formErrors.password ? 'password-error' : undefined}
/>
<button
type="button"
className="password-toggle"
onClick={() => setShowPassword(!showPassword)}
aria-label={showPassword ? 'Hide password' : 'Show password'}
>
{showPassword ? '👁️' : '👁️🗨️'}
</button>
</div>
{formErrors.password && (
<span id="password-error" className="field-error">
{formErrors.password}
</span>
)}
</div>
<div className="form-options">
<label className="remember-me">
<input type="checkbox" />
<span>Remember me</span>
</label>
<a href="/auth/forgot-password" className="forgot-password">
Forgot password?
</a>
</div>
<button type="submit" disabled={isLoading} className="submit-button">
{isLoading ? (
<>
<span className="spinner"></span>
Signing in...
</>
) : (
'Sign In'
)}
</button>
<div className="form-footer">
<p>
Don't have an account?{' '}
<a href="/auth/signup">Sign up</a>
</p>
</div>
</form>
</div>
)
}
import { useSignin, useDeployment } from '@snipextt/wacht'
import { useState } from 'react'
function MultiStrategySignIn() {
const [activeStrategy, setActiveStrategy] = useState('plain_email')
const [email, setEmail] = useState('')
const [password, setPassword] = useState('')
const [phoneNumber, setPhoneNumber] = useState('')
const { signIn, signInWithOAuth, isLoading, error, clearError } = useSignin()
const { authStrategies, socialConnections } = useDeployment()
const handleEmailSignIn = async (e) => {
e.preventDefault()
await signIn({
strategy: 'plain_email',
email,
password
})
}
const handleMagicLink = async (e) => {
e.preventDefault()
await signIn({
strategy: 'magic_link',
email
})
}
const handlePhoneSignIn = async (e) => {
e.preventDefault()
await signIn({
strategy: 'phone_sms',
phoneNumber
})
}
const handleOAuthSignIn = (provider) => {
signInWithOAuth(provider)
}
return (
<div className="multi-strategy-signin">
<h2>Sign In</h2>
{error && (
<div className="error-banner">
{error.message}
<button onClick={clearError}>×</button>
</div>
)}
{/* Strategy Selector */}
<div className="strategy-tabs">
{authStrategies.includes('plain_email') && (
<button
type="button"
className={activeStrategy === 'plain_email' ? 'active' : ''}
onClick={() => setActiveStrategy('plain_email')}
>
Email & Password
</button>
)}
{authStrategies.includes('magic_link') && (
<button
type="button"
className={activeStrategy === 'magic_link' ? 'active' : ''}
onClick={() => setActiveStrategy('magic_link')}
>
Magic Link
</button>
)}
{authStrategies.includes('phone_sms') && (
<button
type="button"
className={activeStrategy === 'phone_sms' ? 'active' : ''}
onClick={() => setActiveStrategy('phone_sms')}
>
Phone Number
</button>
)}
</div>
{/* Email & Password Form */}
{activeStrategy === 'plain_email' && (
<form onSubmit={handleEmailSignIn} className="strategy-form">
<div className="form-group">
<label>Email</label>
<input
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
placeholder="Enter your email"
required
/>
</div>
<div className="form-group">
<label>Password</label>
<input
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
placeholder="Enter your password"
required
/>
</div>
<button type="submit" disabled={isLoading}>
{isLoading ? 'Signing in...' : 'Sign In'}
</button>
</form>
)}
{/* Magic Link Form */}
{activeStrategy === 'magic_link' && (
<form onSubmit={handleMagicLink} className="strategy-form">
<div className="form-group">
<label>Email</label>
<input
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
placeholder="Enter your email"
required
/>
</div>
<button type="submit" disabled={isLoading}>
{isLoading ? 'Sending Magic Link...' : 'Send Magic Link'}
</button>
<p className="strategy-help">
We'll send you a secure link to sign in instantly.
</p>
</form>
)}
{/* Phone SMS Form */}
{activeStrategy === 'phone_sms' && (
<form onSubmit={handlePhoneSignIn} className="strategy-form">
<div className="form-group">
<label>Phone Number</label>
<input
type="tel"
value={phoneNumber}
onChange={(e) => setPhoneNumber(e.target.value)}
placeholder="+1 (555) 123-4567"
required
/>
</div>
<button type="submit" disabled={isLoading}>
{isLoading ? 'Sending Code...' : 'Send Verification Code'}
</button>
<p className="strategy-help">
We'll send you a verification code via SMS.
</p>
</form>
)}
{/* Social Sign-On */}
{socialConnections.length > 0 && (
<div className="social-signin">
<div className="divider">
<span>Or continue with</span>
</div>
<div className="social-buttons">
{socialConnections.map(connection => (
<button
key={connection.provider}
type="button"
onClick={() => handleOAuthSignIn(connection.provider)}
className={`social-button social-${connection.provider}`}
disabled={isLoading}
>
<img
src={`/icons/${connection.provider}.svg`}
alt={connection.provider}
/>
{connection.provider}
</button>
))}
</div>
</div>
)}
</div>
)
}
import { useSignin } from '@snipextt/wacht'
import { useState } from 'react'
function SignInWithRememberMe() {
const [email, setEmail] = useState('')
const [password, setPassword] = useState('')
const [rememberMe, setRememberMe] = useState(false)
const { signIn, isLoading, error } = useSignin()
const handleSubmit = async (e) => {
e.preventDefault()
await signIn({
strategy: 'plain_email',
email,
password,
// Remember me affects session duration
sessionDuration: rememberMe ? '30d' : '1d'
})
}
return (
<form onSubmit={handleSubmit}>
<input
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
placeholder="Email"
required
/>
<input
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
placeholder="Password"
required
/>
<label className="remember-me-checkbox">
<input
type="checkbox"
checked={rememberMe}
onChange={(e) => setRememberMe(e.target.checked)}
/>
Keep me signed in for 30 days
</label>
<button type="submit" disabled={isLoading}>
Sign In
</button>
</form>
)
}
import { useSignin, useNavigation } from '@snipextt/wacht'
import { useState, useEffect } from 'react'
function SignInWithRedirect() {
const [email, setEmail] = useState('')
const [password, setPassword] = useState('')
const { signIn, isLoading, error } = useSignin()
const { navigate } = useNavigation()
// Get intended destination from URL or storage
const [redirectTo, setRedirectTo] = useState('/')
useEffect(() => {
// Check for redirect parameter in URL
const urlParams = new URLSearchParams(window.location.search)
const redirect = urlParams.get('redirect')
// Or check localStorage for intended route
const savedRedirect = localStorage.getItem('intended_route')
if (redirect) {
setRedirectTo(decodeURIComponent(redirect))
} else if (savedRedirect) {
setRedirectTo(savedRedirect)
localStorage.removeItem('intended_route')
}
}, [])
const handleSubmit = async (e) => {
e.preventDefault()
try {
await signIn({
strategy: 'plain_email',
email,
password
})
// Redirect to intended destination
navigate(redirectTo)
} catch (err) {
console.error('Sign-in failed:', err)
}
}
return (
<div className="signin-with-redirect">
<h2>Sign In</h2>
{redirectTo !== '/' && (
<p className="redirect-notice">
You'll be redirected to: <strong>{redirectTo}</strong>
</p>
)}
<form onSubmit={handleSubmit}>
{/* Form fields */}
<button type="submit" disabled={isLoading}>
Sign In
</button>
</form>
</div>
)
}
import { useSignin, useDeployment } from '@snipextt/wacht'
import { useState, useEffect } from 'react'
function ProgressiveSignIn() {
const [currentStep, setCurrentStep] = useState('identifier')
const [identifier, setIdentifier] = useState('')
const [password, setPassword] = useState('')
const [identifierType, setIdentifierType] = useState('')
const { signIn, isLoading, error } = useSignin()
const { deployment } = useDeployment()
// Detect if identifier is email or username
useEffect(() => {
if (identifier.includes('@')) {
setIdentifierType('email')
} else if (identifier.length > 0) {
setIdentifierType('username')
} else {
setIdentifierType('')
}
}, [identifier])
const handleIdentifierSubmit = async (e) => {
e.preventDefault()
// Check if magic link is available for this identifier
if (identifierType === 'email' && deployment.authStrategies.includes('magic_link')) {
// Show both password and magic link options
setCurrentStep('auth_method')
} else {
// Go straight to password
setCurrentStep('password')
}
}
const handlePasswordSignIn = async () => {
const strategy = identifierType === 'email' ? 'plain_email' : 'username'
await signIn({
strategy,
[identifierType]: identifier,
password
})
}
const handleMagicLink = async () => {
await signIn({
strategy: 'magic_link',
email: identifier
})
}
return (
<div className="progressive-signin">
{currentStep === 'identifier' && (
<form onSubmit={handleIdentifierSubmit}>
<h2>Welcome</h2>
<div className="form-group">
<label>Email or Username</label>
<input
type="text"
value={identifier}
onChange={(e) => setIdentifier(e.target.value)}
placeholder="Enter your email or username"
required
/>
</div>
<button type="submit" disabled={!identifier}>
Continue
</button>
</form>
)}
{currentStep === 'auth_method' && (
<div className="auth-method-selection">
<h2>How would you like to sign in?</h2>
<p>Signing in as: <strong>{identifier}</strong></p>
<div className="method-options">
<button
type="button"
onClick={() => setCurrentStep('password')}
className="method-button"
>
<span className="icon">🔑</span>
<div>
<strong>Use Password</strong>
<p>Sign in with your password</p>
</div>
</button>
<button
type="button"
onClick={handleMagicLink}
className="method-button"
disabled={isLoading}
>
<span className="icon">✨</span>
<div>
<strong>Magic Link</strong>
<p>Get a secure link via email</p>
</div>
</button>
</div>
</div>
)}
{currentStep === 'password' && (
<form onSubmit={(e) => { e.preventDefault(); handlePasswordSignIn(); }}>
<h2>Enter Password</h2>
<p>Signing in as: <strong>{identifier}</strong></p>
<div className="form-group">
<label>Password</label>
<input
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
placeholder="Enter your password"
autoFocus
required
/>
</div>
<button type="submit" disabled={isLoading}>
{isLoading ? 'Signing in...' : 'Sign In'}
</button>
<button
type="button"
onClick={() => setCurrentStep('identifier')}
className="back-button"
>
← Back
</button>
</form>
)}
</div>
)
}
import { useSignin } from '@snipextt/wacht'
function ErrorAwareSignIn() {
const { signIn, error, clearError, isLoading } = useSignin()
const getErrorMessage = (error) => {
switch (error.code) {
case 'INVALID_CREDENTIALS':
return {
title: 'Invalid Credentials',
message: 'The email or password you entered is incorrect.',
action: 'Try again or reset your password'
}
case 'ACCOUNT_LOCKED':
return {
title: 'Account Locked',
message: 'Your account has been temporarily locked due to multiple failed attempts.',
action: 'Try again in a few minutes or contact support'
}
case 'EMAIL_NOT_VERIFIED':
return {
title: 'Email Not Verified',
message: 'Please verify your email address before signing in.',
action: 'Check your email for a verification link'
}
case 'RATE_LIMIT_EXCEEDED':
return {
title: 'Too Many Attempts',
message: 'You\'ve made too many sign-in attempts.',
action: 'Please wait a moment before trying again'
}
default:
return {
title: 'Sign-In Error',
message: error.message || 'Something went wrong.',
action: 'Please try again'
}
}
}
if (error) {
const errorInfo = getErrorMessage(error)
return (
<div className="error-state">
<div className="error-icon">⚠️</div>
<h3>{errorInfo.title}</h3>
<p>{errorInfo.message}</p>
<p className="error-action">{errorInfo.action}</p>
<div className="error-actions">
<button onClick={clearError}>
Try Again
</button>
{error.code === 'INVALID_CREDENTIALS' && (
<a href="/auth/forgot-password">
Reset Password
</a>
)}
{error.code === 'EMAIL_NOT_VERIFIED' && (
<button onClick={() => sendVerificationEmail()}>
Resend Verification
</button>
)}
</div>
</div>
)
}
// Regular sign-in form when no error
return <SignInForm />
}
import { render, screen, fireEvent, waitFor } from '@testing-library/react'
import { DeploymentProvider } from '@snipextt/wacht'
import SignInForm from './SignInForm'
const TestWrapper = ({ children }) => (
<DeploymentProvider publicKey="pk_test_key">
{children}
</DeploymentProvider>
)
test('should sign in with valid credentials', async () => {
render(<SignInForm />, { wrapper: TestWrapper })
fireEvent.change(screen.getByLabelText(/email/i), {
target: { value: 'test@example.com' }
})
fireEvent.change(screen.getByLabelText(/password/i), {
target: { value: 'password123' }
})
fireEvent.click(screen.getByRole('button', { name: /sign in/i }))
await waitFor(() => {
expect(mockNavigate).toHaveBeenCalledWith('/dashboard')
})
})
test('should display error for invalid credentials', async () => {
render(<SignInForm />, { wrapper: TestWrapper })
// Simulate sign-in with invalid credentials
fireEvent.change(screen.getByLabelText(/email/i), {
target: { value: 'wrong@example.com' }
})
fireEvent.change(screen.getByLabelText(/password/i), {
target: { value: 'wrongpassword' }
})
fireEvent.click(screen.getByRole('button', { name: /sign in/i }))
await waitFor(() => {
expect(screen.getByText(/invalid credentials/i)).toBeInTheDocument()
})
})