Skip to main content
Docs

Build a custom flow for adding a phone number 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 the Account Portal pages or prebuilt components.

Users are able to add multiple phone numbers to their account.

Every user has a User object that represents their account. The User object has a phoneNumbers property that contains all the phone numbers associated with the user.

The following example demonstrates how to build a custom user interface that allows users to add a phone number to their account. The example:

  1. Uses the useUser() hook to get the User object.
  2. Uses the User.createPhoneNumber() method to add the phone number to the user's account. A new PhoneNumber object is created and stored in User.phoneNumbers.
  3. Uses the prepareVerification() method on the newly created PhoneNumber object to send a verification code to the user.
  4. Uses the attemptVerification() method on the same PhoneNumber object with the verification code provided by the user to verify the phone number.

Warning

Phone numbers must be in E.164 format.

app/account/add-phone/page.tsx
'use client'

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

export default function Page() {
  const { isLoaded, user } = useUser()
  const [phone, setPhone] = React.useState('')
  const [code, setCode] = React.useState('')
  const [isVerifying, setIsVerifying] = React.useState(false)
  const [successful, setSuccessful] = React.useState(false)
  const [phoneObj, setPhoneObj] = React.useState<PhoneNumberResource | 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 phone number
  const handleSubmit = async (e: React.FormEvent) => {
    e.preventDefault()

    try {
      // Add unverified phone number to user
      const res = await user.createPhoneNumber({ phoneNumber: phone })
      // Reload user to get updated User object
      await user.reload()

      // Create a reference to the new phone number to use related methods
      const phoneNumber = user.phoneNumbers.find((a) => a.id === res.id)
      setPhoneObj(phoneNumber)

      // Send the user an SMS with the verification code
      phoneNumber?.prepareVerification()

      // 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 provided code matches the code sent to the user
      const phoneVerifyAttempt = await phoneObj?.attemptVerification({ code })

      if (phoneVerifyAttempt?.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(phoneVerifyAttempt, null, 2))
      }
    } catch (err) {
      console.error(JSON.stringify(err, null, 2))
    }
  }

  // Display a success message if the phone number was added successfully
  if (successful) {
    return (
      <>
        <h1>Phone added</h1>
      </>
    )
  }

  // Display the verification form to capture the OTP code
  if (isVerifying) {
    return (
      <>
        <h1>Verify phone</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 phone number
  return (
    <>
      <h1>Add phone</h1>
      <div>
        <form onSubmit={(e) => handleSubmit(e)}>
          <div>
            <label htmlFor="phone">Enter phone number</label>
            <input
              onChange={(e) => setPhone(e.target.value)}
              id="phone"
              name="phone"
              type="phone"
              value={phone}
            />
          </div>
          <div>
            <button type="submit">Continue</button>
          </div>
        </form>
      </div>
    </>
  )
}
AddPhoneView.swift
import SwiftUI
import ClerkSDK

struct AddPhoneView: View {
  @State private var phone = ""
  @State private var code = ""
  @State private var isVerifying = false
  // Create a reference to the phone number that we'll be creating
  @State private var newPhoneNumber: PhoneNumber?

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

    if isVerifying {
      TextField("Enter code", text: $code)
      Button("Verify") {
        Task { await verifyCode(code) }
      }
    } else {
      TextField("Enter phone number", text: $phone)
      Button("Continue") {
        Task { await createPhone(phone) }
      }
    }
  }
}

extension AddPhoneView {

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

      // Add an unverified phone number to user
      self.newPhoneNumber = try await user.createPhoneNumber(phone)

      guard let newphoneNumber = self.newPhoneNumber else { return }

      // Send the user an sms message with the verification code
      try await newphoneNumber.prepareVerification()

      // 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 newPhoneNumber else { return }

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

      // If the status is not complete, check why. User may need to
      // complete further steps.
      dump(self.newPhoneNumber?.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