Overview

This guide shows you how to build comprehensive sign-up flows from scratch using Wacht’s React SDK. You’ll learn to create registration forms, handle validation, manage verification flows, and implement smooth onboarding experiences.

Basic Sign-Up Flow

1. Simple Registration Form

Start with a basic sign-up form using the 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>
  )
}

2. Enhanced Form with Validation

Add comprehensive validation and better UX:
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>
  )
}

Multi-Step Registration

Step-by-Step Sign-Up Flow

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>
  )
}

Invitation-Based Sign-Up

Organization Invitation Flow

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>
  )
}

Email Verification Flow

Post-Registration Verification

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>
  )
}

Testing Sign-Up Flows

Component Testing

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()
  })
})

Next Steps