Docs

Build a custom flow for adding an email to a user's account

Warning

This guide is for users who want to build a custom user interface using the Clerk API. To use a prebuilt UI, use Clerk's Account Portal pages or prebuilt components.

Users are able to add multiple email addresses to their account.

Every user has a User object that represents their account. The User object has a emailAddresses property that contains all the email addresses associated with the user.

The following example demonstrates how to build a custom user interface that allows users to add an email address to their account. The example:

  1. Uses the useUser() hook to get the User object.
  2. Uses the User.createEmailAddress() method to add the email address to the user's account. A new EmailAddress object is created and stored in User.emailAddresses.
  3. Uses the prepareVerification() method on the newly created EmailAddress object to send a verification code to the user.
  4. Uses the attemptVerification() method on the same EmailAddress object with the verification code provided by the user to verify the email address.
app/account/add-email/page.tsx
'use client'

import * as React from 'react'
import { useUser } from '@clerk/nextjs'
import { EmailAddressResource } from '@clerk/types'

export default function Page() {
  const { isLoaded, user } = useUser()
  const [email, setEmail] = React.useState('')
  const [code, setCode] = React.useState('')
  const [isVerifying, setIsVerifying] = React.useState(false)
  const [successful, setSuccessful] = React.useState(false)
  const [emailObj, setEmailObj] = React.useState<EmailAddressResource | undefined>()

  if (!isLoaded) return null

  if (isLoaded && !user?.id) {
    return <p>You must be logged in to access this page</p>
  }

  // Handle addition of the email address
  const handleSubmit = async (e: React.FormEvent) => {
    e.preventDefault()

    try {
      // Add an unverified email address to user
      const res = await user.createEmailAddress({ email })
      // Reload user to get updated User object
      await user.reload()

      // Find the email address that was just added
      const emailAddress = user.emailAddresses.find((a) => a.id === res.id)
      // Create a reference to the email address that was just added
      setEmailObj(emailAddress)

      // Send the user an email with the verification code
      emailAddress?.prepareVerification({ strategy: 'email_code' })

      // Set to true to display second form
      // and capture the OTP code
      setIsVerifying(true)
    } catch (err) {
      // See https://clerk.com/docs/custom-flows/error-handling
      // for more info on error handling
      console.error(JSON.stringify(err, null, 2))
    }
  }

  // Handle the submission of the verification form
  const verifyCode = async (e: React.FormEvent) => {
    e.preventDefault()
    try {
      // Verify that the code entered matches the code sent to the user
      const emailVerifyAttempt = await emailObj?.attemptVerification({ code })

      if (emailVerifyAttempt?.verification.status === 'verified') {
        setSuccessful(true)
      } else {
        // If the status is not complete, check why. User may need to
        // complete further steps.
        console.error(JSON.stringify(emailVerifyAttempt, null, 2))
      }
    } catch (err) {
      console.error(JSON.stringify(err, null, 2))
    }
  }

  // Display a success message if the email was added successfully
  if (successful) {
    return (
      <>
        <h1>Email added!</h1>
      </>
    )
  }

  // Display the verification form to capture the OTP code
  if (isVerifying) {
    return (
      <>
        <h1>Verify email</h1>
        <div>
          <form onSubmit={(e) => verifyCode(e)}>
            <div>
              <label htmlFor="code">Enter code</label>
              <input
                onChange={(e) => setCode(e.target.value)}
                id="code"
                name="code"
                type="text"
                value={code}
              />
            </div>
            <div>
              <button type="submit">Verify</button>
            </div>
          </form>
        </div>
      </>
    )
  }

  // Display the initial form to capture the email address
  return (
    <>
      <h1>Add Email</h1>
      <div>
        <form onSubmit={(e) => handleSubmit(e)}>
          <div>
            <label htmlFor="email">Enter email address</label>
            <input
              onChange={(e) => setEmail(e.target.value)}
              id="email"
              name="email"
              type="email"
              value={email}
            />
          </div>
          <div>
            <button type="submit">Continue</button>
          </div>
        </form>
      </div>
    </>
  )
}
AddEmailView.swift
import SwiftUI
import ClerkSDK

struct AddEmailView: View {
  @State private var email = ""
  @State private var code = ""
  @State private var isVerifying = false
  // Create a reference to the email address that we'll be creating
  @State private var newEmailAddress: EmailAddress?

  var body: some View {
    if newEmailAddress?.verification?.status == .verified {
      Text("Email added!")
    }

    if isVerifying {
      TextField("Enter code", text: $code)
      Button("Verify") {
        Task { await verifyCode(code) }
      }
    } else {
      TextField("Enter email address", text: $email)
      Button("Continue") {
        Task { await createEmail(email) }
      }
    }
  }
}

extension AddEmailView {

  func createEmail(_ email: String) async {
    do {
      guard let user = Clerk.shared.user else { return }

      // Add an unverified email address to user
      self.newEmailAddress = try await user.createEmailAddress(email)

      guard let newEmailAddress = self.newEmailAddress else { return }

      // Send the user an email with the verification code
      try await newEmailAddress.prepareVerification(strategy: .emailCode)

      // Set to true to display second form
      // and capture the OTP code
      isVerifying = true
    } catch {
      // See https://clerk.com/docs/custom-flows/error-handling
      // for more info on error handling
      dump(error)
    }
  }

  func verifyCode(_ code: String) async {
    do {
      guard let newEmailAddress else { return }

      // Verify that the code entered matches the code sent to the user
      self.newEmailAddress = try await newEmailAddress.attemptVerification(strategy: .emailCode(code: code))

      // If the status is not complete, check why. User may need to
      // complete further steps.
      dump(self.newEmailAddress?.verification?.status)
    } catch {
      // See https://clerk.com/docs/custom-flows/error-handling
      // for more info on error handling
      dump(error)
    }
  }
}

Feedback

What did you think of this content?

Last updated on