Build a custom flow for adding a phone number to a user's account
Users are able to add multiple phone numbers to their account. Adding a phone number requires the user to verify the phone number before it can be added to the user's account.
This guide demonstrates how to build a custom user interface that allows users to add and verify a phone number for their account.
Configure phone number verification
To use phone number verification, you first need to enable it for your application.
- In the Clerk Dashboard, navigate to the User & authentication page.
- Select the Phone tab and enable Add phone to account.
Phone number code verification
- Every user has a User object that represents their account. The Userobject has aphoneNumbersproperty that contains all the phone numbers associated with the user. The useUser() hook is used to get theUserobject.
- The User.createPhoneNumber() method is passed to the useReverification() hook to require the user to reverify their credentials before being able to add a phone number to their account.
- If the createPhoneNumber()function is successful, a new PhoneNumber object is created and stored inUser.phoneNumbers.
- Uses the prepareVerification()method on the newly createdPhoneNumberobject to send a verification code to the user.
- Uses the attemptVerification()method on the samePhoneNumberobject with the verification code provided by the user to verify the phone number.
'use client'
import * as React from 'react'
import { useUser, useReverification } from '@clerk/nextjs'
import { PhoneNumberResource } from '@clerk/types'
export default function Page() {
  const { isLoaded, isSignedIn, 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>()
  const createPhoneNumber = useReverification((phone: string) =>
    user?.createPhoneNumber({ phoneNumber: phone }),
  )
  if (!isLoaded) {
    // Handle loading state
    return null
  }
  if (!isSignedIn) {
    // Handle signed out state
    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 createPhoneNumber(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/guides/development/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>
    </>
  )
}import SwiftUI
import Clerk
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,
      // then send the user an sms message with the verification code
      self.newPhoneNumber = try await user
        .createPhoneNumber(phone)
        .prepareVerification()
      // Set to true to display second form
      // and capture the OTP code
      isVerifying = true
    } catch {
      // See https://clerk.com/docs/guides/development/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/guides/development/custom-flows/error-handling
      // for more info on error handling
      dump(error)
    }
  }
}import android.util.Log
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.clerk.api.Clerk
import com.clerk.api.network.serialization.flatMap
import com.clerk.api.network.serialization.longErrorMessageOrNull
import com.clerk.api.network.serialization.onFailure
import com.clerk.api.network.serialization.onSuccess
import com.clerk.api.phonenumber.PhoneNumber
import com.clerk.api.phonenumber.attemptVerification
import com.clerk.api.phonenumber.prepareVerification
import com.clerk.api.user.createPhoneNumber
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.launch
class AddPhoneViewModel : ViewModel() {
  private val _uiState = MutableStateFlow<UiState>(UiState.NeedsVerification)
  val uiState = _uiState.asStateFlow()
  init {
    combine(Clerk.isInitialized, Clerk.userFlow) { isInitialized, user ->
        _uiState.value =
          when {
            !isInitialized -> UiState.Loading
            user == null -> UiState.SignedOut
            else -> UiState.NeedsVerification
          }
      }
      .launchIn(viewModelScope)
  }
  fun createPhoneNumber(phoneNumber: String) {
    val user = requireNotNull(Clerk.userFlow.value)
    // Add an unverified phone number to the user,
    // then send the user an SMS with the verification code
    viewModelScope.launch {
      user
        .createPhoneNumber(phoneNumber)
        .flatMap { it.prepareVerification() }
        .onSuccess {
          // Update the state to show that the phone number has been created
          // and that the user needs to verify the phone number
          _uiState.value = UiState.Verifying(it)
        }
        .onFailure {
          Log.e(
            "AddPhoneViewModel",
            "Failed to create phone number and prepare verification: ${it.longErrorMessageOrNull}",
          )
        }
    }
  }
  fun verifyCode(code: String, newPhoneNumber: PhoneNumber) {
    viewModelScope.launch {
      newPhoneNumber
        .attemptVerification(code)
        .onSuccess {
          // Update the state to show that the phone number has been verified
          _uiState.value = UiState.Verified
        }
        .onFailure {
          Log.e("AddPhoneViewModel", "Failed to verify phone number: ${it.longErrorMessageOrNull}")
        }
    }
  }
  sealed interface UiState {
    data object Loading : UiState
    data object NeedsVerification : UiState
    data class Verifying(val phoneNumber: PhoneNumber) : UiState
    data object Verified : UiState
    data object SignedOut : UiState
  }
}import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.viewModels
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Button
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.Text
import androidx.compose.material3.TextField
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.clerk.api.phonenumber.PhoneNumber
class AddPhoneActivity : ComponentActivity() {
  val viewModel: AddPhoneViewModel by viewModels()
  override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContent {
      val state by viewModel.uiState.collectAsStateWithLifecycle()
      AddPhoneView(
        state = state,
        onCreatePhoneNumber = viewModel::createPhoneNumber,
        onVerifyCode = viewModel::verifyCode,
      )
    }
  }
}
@Composable
fun AddPhoneView(
  state: AddPhoneViewModel.UiState,
  onCreatePhoneNumber: (String) -> Unit,
  onVerifyCode: (String, PhoneNumber) -> Unit,
) {
  Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
    when (state) {
      AddPhoneViewModel.UiState.NeedsVerification -> {
        InputContentView(buttonText = "Continue", placeholder = "Enter phone number") {
          onCreatePhoneNumber(it)
        }
      }
      AddPhoneViewModel.UiState.Verified -> Text("Verified!")
      is AddPhoneViewModel.UiState.Verifying -> {
        InputContentView(buttonText = "Verify", placeholder = "Enter code") {
          onVerifyCode(it, state.phoneNumber)
        }
      }
      AddPhoneViewModel.UiState.Loading -> CircularProgressIndicator()
      AddPhoneViewModel.UiState.SignedOut -> Text("You must be signed in to add a phone number.")
    }
  }
}
@Composable
fun InputContentView(
  buttonText: String,
  placeholder: String,
  modifier: Modifier = Modifier,
  onClick: (String) -> Unit,
) {
  var input by remember { mutableStateOf("") }
  Column(horizontalAlignment = Alignment.CenterHorizontally, modifier = modifier) {
    TextField(
      modifier = Modifier.padding(bottom = 16.dp),
      value = input,
      onValueChange = { input = it },
      placeholder = { Text(placeholder) },
    )
    Button(onClick = { onClick(input) }) { Text(buttonText) }
  }
}Feedback
Last updated on