Complete guide to building custom user registration flows with validation, verification, and onboarding
useSignup
hook:
import { useSignup, useNavigation } from '@snipextt/wacht'
import { useState } from 'react'
function BasicSignUpForm() {
const [formData, setFormData] = useState({
firstName: '',
lastName: '',
email: '',
password: ''
})
const { signUp, isLoading, error, clearError } = useSignup()
const { navigate } = useNavigation()
const handleSubmit = async (e) => {
e.preventDefault()
clearError()
try {
await signUp({
first_name: formData.firstName,
last_name: formData.lastName,
email: formData.email,
password: formData.password
})
// Success - redirect to verification or dashboard
navigate('/verify-email')
} catch (err) {
console.error('Sign-up failed:', err)
}
}
const handleChange = (field) => (e) => {
setFormData(prev => ({
...prev,
[field]: e.target.value
}))
}
return (
<form onSubmit={handleSubmit} className="signup-form">
<h2>Create Account</h2>
{error && (
<div className="error-banner">
<span>{error.message}</span>
<button type="button" onClick={clearError}>×</button>
</div>
)}
<div className="form-row">
<div className="form-group">
<label htmlFor="firstName">First Name</label>
<input
id="firstName"
type="text"
value={formData.firstName}
onChange={handleChange('firstName')}
placeholder="Enter your first name"
required
/>
</div>
<div className="form-group">
<label htmlFor="lastName">Last Name</label>
<input
id="lastName"
type="text"
value={formData.lastName}
onChange={handleChange('lastName')}
placeholder="Enter your last name"
required
/>
</div>
</div>
<div className="form-group">
<label htmlFor="email">Email</label>
<input
id="email"
type="email"
value={formData.email}
onChange={handleChange('email')}
placeholder="Enter your email"
required
/>
</div>
<div className="form-group">
<label htmlFor="password">Password</label>
<input
id="password"
type="password"
value={formData.password}
onChange={handleChange('password')}
placeholder="Create a password"
required
/>
</div>
<button type="submit" disabled={isLoading}>
{isLoading ? 'Creating Account...' : 'Create Account'}
</button>
<div className="form-footer">
<p>
Already have an account?{' '}
<a href="/auth/signin">Sign in</a>
</p>
</div>
</form>
)
}
import { useSignup } from '@snipextt/wacht'
import { useState, useEffect } from 'react'
function EnhancedSignUpForm() {
const [formData, setFormData] = useState({
firstName: '',
lastName: '',
email: '',
password: '',
confirmPassword: ''
})
const [formErrors, setFormErrors] = useState({})
const [passwordStrength, setPasswordStrength] = useState(0)
const [acceptedTerms, setAcceptedTerms] = useState(false)
const { signUp, isLoading, error, validationErrors, clearError } = useSignup()
// Password strength calculator
useEffect(() => {
const password = formData.password
let strength = 0
if (password.length >= 8) strength += 1
if (/[A-Z]/.test(password)) strength += 1
if (/[a-z]/.test(password)) strength += 1
if (/[0-9]/.test(password)) strength += 1
if (/[^A-Za-z0-9]/.test(password)) strength += 1
setPasswordStrength(strength)
}, [formData.password])
const validateForm = () => {
const errors = {}
if (!formData.firstName.trim()) {
errors.firstName = 'First name is required'
}
if (!formData.lastName.trim()) {
errors.lastName = 'Last name is required'
}
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 < 8) {
errors.password = 'Password must be at least 8 characters'
} else if (passwordStrength < 3) {
errors.password = 'Password is too weak'
}
if (formData.password !== formData.confirmPassword) {
errors.confirmPassword = 'Passwords do not match'
}
if (!acceptedTerms) {
errors.terms = 'You must accept the terms and conditions'
}
setFormErrors(errors)
return Object.keys(errors).length === 0
}
const handleSubmit = async (e) => {
e.preventDefault()
if (!validateForm()) return
try {
await signUp({
first_name: formData.firstName,
last_name: formData.lastName,
email: formData.email,
password: formData.password
})
} catch (err) {
console.error('Sign-up failed:', err)
}
}
const handleChange = (field) => (e) => {
setFormData(prev => ({ ...prev, [field]: e.target.value }))
// Clear field error when user starts typing
if (formErrors[field]) {
setFormErrors(prev => ({ ...prev, [field]: '' }))
}
// Clear server errors
if (error) clearError()
}
const getPasswordStrengthColor = () => {
if (passwordStrength <= 2) return '#ef4444' // red
if (passwordStrength === 3) return '#f59e0b' // yellow
return '#10b981' // green
}
const getPasswordStrengthText = () => {
if (passwordStrength <= 2) return 'Weak'
if (passwordStrength === 3) return 'Fair'
if (passwordStrength === 4) return 'Good'
return 'Strong'
}
return (
<div className="signup-container">
<form onSubmit={handleSubmit} className="signup-form">
<div className="form-header">
<h2>Create Your Account</h2>
<p>Join thousands of users already using our platform</p>
</div>
{error && (
<div className="error-banner">
<span>{error.message}</span>
<button type="button" onClick={clearError}>×</button>
</div>
)}
<div className="form-row">
<div className="form-group">
<label htmlFor="firstName">First Name</label>
<input
id="firstName"
type="text"
value={formData.firstName}
onChange={handleChange('firstName')}
placeholder="John"
className={formErrors.firstName ? 'error' : ''}
aria-invalid={!!formErrors.firstName}
/>
{formErrors.firstName && (
<span className="field-error">{formErrors.firstName}</span>
)}
</div>
<div className="form-group">
<label htmlFor="lastName">Last Name</label>
<input
id="lastName"
type="text"
value={formData.lastName}
onChange={handleChange('lastName')}
placeholder="Doe"
className={formErrors.lastName ? 'error' : ''}
aria-invalid={!!formErrors.lastName}
/>
{formErrors.lastName && (
<span className="field-error">{formErrors.lastName}</span>
)}
</div>
</div>
<div className="form-group">
<label htmlFor="email">Email Address</label>
<input
id="email"
type="email"
value={formData.email}
onChange={handleChange('email')}
placeholder="john@example.com"
className={formErrors.email || validationErrors.email ? 'error' : ''}
aria-invalid={!!(formErrors.email || validationErrors.email)}
/>
{(formErrors.email || validationErrors.email) && (
<span className="field-error">
{formErrors.email || validationErrors.email}
</span>
)}
</div>
<div className="form-group">
<label htmlFor="password">Password</label>
<input
id="password"
type="password"
value={formData.password}
onChange={handleChange('password')}
placeholder="Create a strong password"
className={formErrors.password ? 'error' : ''}
aria-invalid={!!formErrors.password}
/>
{formData.password && (
<div className="password-strength">
<div className="strength-bar">
<div
className="strength-fill"
style={{
width: `${(passwordStrength / 5) * 100}%`,
backgroundColor: getPasswordStrengthColor()
}}
/>
</div>
<span
className="strength-text"
style={{ color: getPasswordStrengthColor() }}
>
{getPasswordStrengthText()}
</span>
</div>
)}
{formErrors.password && (
<span className="field-error">{formErrors.password}</span>
)}
<div className="password-requirements">
<p>Password must contain:</p>
<ul>
<li className={formData.password.length >= 8 ? 'valid' : ''}>
At least 8 characters
</li>
<li className={/[A-Z]/.test(formData.password) ? 'valid' : ''}>
One uppercase letter
</li>
<li className={/[a-z]/.test(formData.password) ? 'valid' : ''}>
One lowercase letter
</li>
<li className={/[0-9]/.test(formData.password) ? 'valid' : ''}>
One number
</li>
</ul>
</div>
</div>
<div className="form-group">
<label htmlFor="confirmPassword">Confirm Password</label>
<input
id="confirmPassword"
type="password"
value={formData.confirmPassword}
onChange={handleChange('confirmPassword')}
placeholder="Confirm your password"
className={formErrors.confirmPassword ? 'error' : ''}
aria-invalid={!!formErrors.confirmPassword}
/>
{formErrors.confirmPassword && (
<span className="field-error">{formErrors.confirmPassword}</span>
)}
</div>
<div className="form-group">
<label className="checkbox-label">
<input
type="checkbox"
checked={acceptedTerms}
onChange={(e) => setAcceptedTerms(e.target.checked)}
aria-invalid={!!formErrors.terms}
/>
<span className="checkbox-text">
I agree to the{' '}
<a href="/terms" target="_blank" rel="noopener noreferrer">
Terms of Service
</a>{' '}
and{' '}
<a href="/privacy" target="_blank" rel="noopener noreferrer">
Privacy Policy
</a>
</span>
</label>
{formErrors.terms && (
<span className="field-error">{formErrors.terms}</span>
)}
</div>
<button
type="submit"
disabled={isLoading || !acceptedTerms}
className="submit-button"
>
{isLoading ? (
<>
<span className="spinner"></span>
Creating Account...
</>
) : (
'Create Account'
)}
</button>
<div className="form-footer">
<p>
Already have an account?{' '}
<a href="/auth/signin">Sign in instead</a>
</p>
</div>
</form>
</div>
)
}
import { useSignup } from '@snipextt/wacht'
import { useState } from 'react'
function MultiStepSignUp() {
const [currentStep, setCurrentStep] = useState(1)
const [formData, setFormData] = useState({
// Step 1: Basic info
firstName: '',
lastName: '',
email: '',
// Step 2: Security
password: '',
confirmPassword: '',
// Step 3: Profile
company: '',
role: '',
teamSize: '',
// Step 4: Preferences
newsletter: true,
productUpdates: true,
acceptedTerms: false
})
const { signUp, isLoading, error } = useSignup()
const updateFormData = (updates) => {
setFormData(prev => ({ ...prev, ...updates }))
}
const nextStep = () => setCurrentStep(prev => prev + 1)
const prevStep = () => setCurrentStep(prev => prev - 1)
const handleFinalSubmit = async () => {
try {
await signUp({
first_name: formData.firstName,
last_name: formData.lastName,
email: formData.email,
password: formData.password,
// Additional fields can be saved to user metadata
metadata: {
company: formData.company,
role: formData.role,
teamSize: formData.teamSize,
preferences: {
newsletter: formData.newsletter,
productUpdates: formData.productUpdates
}
}
})
} catch (err) {
console.error('Sign-up failed:', err)
}
}
return (
<div className="multi-step-signup">
{/* Progress indicator */}
<div className="progress-bar">
<div className="progress-steps">
{[1, 2, 3, 4].map(step => (
<div
key={step}
className={`step ${currentStep >= step ? 'completed' : ''} ${currentStep === step ? 'active' : ''}`}
>
<span className="step-number">{step}</span>
<span className="step-label">
{step === 1 && 'Basic Info'}
{step === 2 && 'Security'}
{step === 3 && 'Profile'}
{step === 4 && 'Preferences'}
</span>
</div>
))}
</div>
<div
className="progress-fill"
style={{ width: `${(currentStep / 4) * 100}%` }}
/>
</div>
{/* Step content */}
<div className="step-content">
{currentStep === 1 && (
<BasicInfoStep
data={formData}
onChange={updateFormData}
onNext={nextStep}
/>
)}
{currentStep === 2 && (
<SecurityStep
data={formData}
onChange={updateFormData}
onNext={nextStep}
onPrev={prevStep}
/>
)}
{currentStep === 3 && (
<ProfileStep
data={formData}
onChange={updateFormData}
onNext={nextStep}
onPrev={prevStep}
/>
)}
{currentStep === 4 && (
<PreferencesStep
data={formData}
onChange={updateFormData}
onPrev={prevStep}
onSubmit={handleFinalSubmit}
isLoading={isLoading}
error={error}
/>
)}
</div>
</div>
)
}
// Individual step components
function BasicInfoStep({ data, onChange, onNext }) {
const [errors, setErrors] = useState({})
const validate = () => {
const newErrors = {}
if (!data.firstName) newErrors.firstName = 'First name is required'
if (!data.lastName) newErrors.lastName = 'Last name is required'
if (!data.email) newErrors.email = 'Email is required'
else if (!/\S+@\S+\.\S+/.test(data.email)) newErrors.email = 'Invalid email'
setErrors(newErrors)
return Object.keys(newErrors).length === 0
}
const handleNext = () => {
if (validate()) onNext()
}
return (
<div className="step-form">
<h2>Let's start with the basics</h2>
<p>Tell us a bit about yourself</p>
<div className="form-row">
<div className="form-group">
<label>First Name</label>
<input
type="text"
value={data.firstName}
onChange={(e) => onChange({ firstName: e.target.value })}
placeholder="John"
className={errors.firstName ? 'error' : ''}
/>
{errors.firstName && <span className="error">{errors.firstName}</span>}
</div>
<div className="form-group">
<label>Last Name</label>
<input
type="text"
value={data.lastName}
onChange={(e) => onChange({ lastName: e.target.value })}
placeholder="Doe"
className={errors.lastName ? 'error' : ''}
/>
{errors.lastName && <span className="error">{errors.lastName}</span>}
</div>
</div>
<div className="form-group">
<label>Email Address</label>
<input
type="email"
value={data.email}
onChange={(e) => onChange({ email: e.target.value })}
placeholder="john@example.com"
className={errors.email ? 'error' : ''}
/>
{errors.email && <span className="error">{errors.email}</span>}
</div>
<div className="step-actions">
<button onClick={handleNext} className="btn-primary">
Continue
</button>
</div>
</div>
)
}
import { useSignup, useOrganization } from '@snipextt/wacht'
import { useState, useEffect } from 'react'
function InvitationSignUp() {
const [inviteData, setInviteData] = useState(null)
const [formData, setFormData] = useState({
firstName: '',
lastName: '',
password: ''
})
const { signUp, isLoading, error } = useSignup()
const { acceptInvitation } = useOrganization()
// Get invitation token from URL
const inviteToken = new URLSearchParams(window.location.search).get('invite')
// Load invitation details
useEffect(() => {
if (inviteToken) {
// Fetch invitation details
fetchInvitationDetails(inviteToken)
.then(setInviteData)
.catch(console.error)
}
}, [inviteToken])
const handleSubmit = async (e) => {
e.preventDefault()
try {
// Create account
const user = await signUp({
first_name: formData.firstName,
last_name: formData.lastName,
email: inviteData.email, // Pre-filled from invitation
password: formData.password
})
// Accept organization invitation
if (inviteToken) {
await acceptInvitation(inviteToken)
}
// Success - redirect to organization
window.location.href = `/org/${inviteData.organizationId}`
} catch (err) {
console.error('Invitation sign-up failed:', err)
}
}
if (!inviteData) {
return <div>Loading invitation...</div>
}
return (
<div className="invitation-signup">
<div className="invitation-header">
<img
src={inviteData.organization.logoUrl || '/default-org.png'}
alt={inviteData.organization.name}
className="org-logo"
/>
<h1>Join {inviteData.organization.name}</h1>
<p>
<strong>{inviteData.inviterName}</strong> has invited you to join{' '}
<strong>{inviteData.organization.name}</strong> as a{' '}
<strong>{inviteData.role}</strong>
</p>
</div>
<form onSubmit={handleSubmit} className="invite-form">
<div className="form-group">
<label>Email Address</label>
<input
type="email"
value={inviteData.email}
disabled
className="disabled"
/>
<p className="help-text">This email was invited to the organization</p>
</div>
<div className="form-row">
<div className="form-group">
<label>First Name</label>
<input
type="text"
value={formData.firstName}
onChange={(e) => setFormData(prev => ({ ...prev, firstName: e.target.value }))}
placeholder="John"
required
/>
</div>
<div className="form-group">
<label>Last Name</label>
<input
type="text"
value={formData.lastName}
onChange={(e) => setFormData(prev => ({ ...prev, lastName: e.target.value }))}
placeholder="Doe"
required
/>
</div>
</div>
<div className="form-group">
<label>Password</label>
<input
type="password"
value={formData.password}
onChange={(e) => setFormData(prev => ({ ...prev, password: e.target.value }))}
placeholder="Create a secure password"
required
minLength={8}
/>
</div>
<button type="submit" disabled={isLoading} className="btn-primary">
{isLoading ? 'Creating Account...' : 'Accept Invitation & Create Account'}
</button>
{error && (
<div className="error-message">
{error.message}
</div>
)}
</form>
<div className="invitation-footer">
<p>
By creating an account, you agree to our{' '}
<a href="/terms">Terms of Service</a> and{' '}
<a href="/privacy">Privacy Policy</a>
</p>
</div>
</div>
)
}
import { useVerification, useSession } from '@snipextt/wacht'
import { useState, useEffect } from 'react'
function EmailVerificationFlow() {
const [verificationSent, setVerificationSent] = useState(false)
const [resendCooldown, setResendCooldown] = useState(0)
const { prepareVerification, error } = useVerification()
const { session, refetch } = useSession()
const userEmail = session?.active_signin?.user?.email_addresses?.[0]?.email_address
// Cooldown timer for resend button
useEffect(() => {
if (resendCooldown > 0) {
const timer = setTimeout(() => setResendCooldown(prev => prev - 1), 1000)
return () => clearTimeout(timer)
}
}, [resendCooldown])
const sendVerificationEmail = async () => {
try {
await prepareVerification({
strategy: 'email_code',
email_address: userEmail
})
setVerificationSent(true)
setResendCooldown(60) // 60 second cooldown
} catch (err) {
console.error('Failed to send verification:', err)
}
}
// Check if email is already verified
const isEmailVerified = session?.active_signin?.user?.email_addresses?.[0]?.verified
if (isEmailVerified) {
return (
<div className="verification-success">
<div className="success-icon">✅</div>
<h2>Email Verified!</h2>
<p>Your email address has been successfully verified.</p>
<a href="/dashboard" className="btn-primary">
Continue to Dashboard
</a>
</div>
)
}
return (
<div className="email-verification">
<div className="verification-container">
<div className="verification-icon">📧</div>
<h2>Verify Your Email</h2>
{!verificationSent ? (
<div className="verification-prompt">
<p>
Please verify your email address to complete your account setup.
</p>
<p className="email-display">
We'll send a verification link to: <strong>{userEmail}</strong>
</p>
<button
onClick={sendVerificationEmail}
className="btn-primary"
>
Send Verification Email
</button>
</div>
) : (
<div className="verification-sent">
<h3>Check Your Email</h3>
<p>
We've sent a verification link to <strong>{userEmail}</strong>
</p>
<p>
Click the link in your email to verify your account.
</p>
<div className="verification-actions">
<button
onClick={() => refetch()}
className="btn-secondary"
>
I've Verified My Email
</button>
<button
onClick={sendVerificationEmail}
disabled={resendCooldown > 0}
className="btn-text"
>
{resendCooldown > 0
? `Resend in ${resendCooldown}s`
: 'Resend Email'
}
</button>
</div>
</div>
)}
{error && (
<div className="error-message">
{error.message}
</div>
)}
<div className="verification-help">
<details>
<summary>Didn't receive the email?</summary>
<div className="help-content">
<ul>
<li>Check your spam/junk folder</li>
<li>Make sure {userEmail} is correct</li>
<li>Try resending the verification email</li>
<li>Contact support if you continue having issues</li>
</ul>
</div>
</details>
</div>
</div>
</div>
)
}
import { render, screen, fireEvent, waitFor } from '@testing-library/react'
import { DeploymentProvider } from '@snipextt/wacht'
import SignUpForm from './SignUpForm'
const TestWrapper = ({ children }) => (
<DeploymentProvider publicKey="pk_test_key">
{children}
</DeploymentProvider>
)
test('should create account with valid data', async () => {
render(<SignUpForm />, { wrapper: TestWrapper })
fireEvent.change(screen.getByLabelText(/first name/i), {
target: { value: 'John' }
})
fireEvent.change(screen.getByLabelText(/last name/i), {
target: { value: 'Doe' }
})
fireEvent.change(screen.getByLabelText(/email/i), {
target: { value: 'john@example.com' }
})
fireEvent.change(screen.getByLabelText(/password/i), {
target: { value: 'SecurePass123!' }
})
fireEvent.click(screen.getByRole('button', { name: /create account/i }))
await waitFor(() => {
expect(mockNavigate).toHaveBeenCalledWith('/verify-email')
})
})
test('should show validation errors for invalid data', async () => {
render(<SignUpForm />, { wrapper: TestWrapper })
fireEvent.click(screen.getByRole('button', { name: /create account/i }))
await waitFor(() => {
expect(screen.getByText(/first name is required/i)).toBeInTheDocument()
expect(screen.getByText(/email is required/i)).toBeInTheDocument()
})
})