Skip to main content
Docs

Build a custom flow for managing multi-factor authentication

Warning

This guide is for users who want to build a . To use a prebuilt UI, use the Account Portal pages or prebuilt components.

Multi-factor verification (MFA) is an added layer of security that requires users to provide a second verification factor to access an account.

Clerk supports MFA through SMS verification code, Authenticator application, and Backup codes.

This guide will walk you through how to build a custom flow that allows users to manage their MFA settings:

SMS verification code

Enable multi-factor authentication

For your users to be able to enable MFA for their account, you need to enable MFA for your application.

  1. In the Clerk Dashboard, navigate to the Multi-factor page.
  2. For the purpose of this guide, toggle on both the SMS verification code and Backup codes strategies.
  3. Select Save.

Build the custom flow

This example consists of two pages:

  • The main page where users can manage their SMS MFA settings
  • The page where users can add a phone number to their account

Use the following tabs to view the code necessary for each page.

This example is written for Next.js App Router but it can be adapted for any React-based framework, such as React Router or Tanstack React Start.

app/account/manage-mfa/page.tsx
'use client'

import * as React from 'react'
import { useUser, useReverification } from '@clerk/nextjs'
import { BackupCodeResource, PhoneNumberResource } from '@clerk/types'
import Link from 'next/link'

// Display phone numbers reserved for MFA
const ManageMfaPhoneNumbers = () => {
  const { isSignedIn, user } = useUser()

  if (!isSignedIn) {
    // Handle signed out state
    return null
  }

  // Check if any phone numbers are reserved for MFA
  const mfaPhones = user.phoneNumbers
    .filter((ph) => ph.verification.status === 'verified')
    .filter((ph) => ph.reservedForSecondFactor)
    .sort((ph: PhoneNumberResource) => (ph.defaultSecondFactor ? -1 : 1))

  if (mfaPhones.length === 0) {
    return <p>There are currently no phone numbers reserved for MFA.</p>
  }

  return (
    <>
      <h2>Phone numbers reserved for MFA</h2>
      <ul>
        {mfaPhones.map((phone) => {
          return (
            <li key={phone.id} style={{ display: 'flex', gap: '10px' }}>
              <p>
                {phone.phoneNumber} {phone.defaultSecondFactor && '(Default)'}
              </p>
              <div>
                <button onClick={() => phone.setReservedForSecondFactor({ reserved: false })}>
                  Disable for MFA
                </button>
              </div>

              {!phone.defaultSecondFactor && (
                <div>
                  <button onClick={() => phone.makeDefaultSecondFactor()}>Make default</button>
                </div>
              )}

              <div>
                <button onClick={() => phone.destroy()}>Remove from account</button>
              </div>
            </li>
          )
        })}
      </ul>
    </>
  )
}

// Display phone numbers that are not reserved for MFA
const ManageAvailablePhoneNumbers = () => {
  const { isSignedIn, user } = useUser()
  const setReservedForSecondFactor = useReverification((phone: PhoneNumberResource) =>
    phone.setReservedForSecondFactor({ reserved: true }),
  )
  const destroyPhone = useReverification((phone: PhoneNumberResource) => phone.destroy())

  if (!isSignedIn) {
    // Handle signed out state
    return null
  }

  // Check if any phone numbers aren't reserved for MFA
  const availableForMfaPhones = user.phoneNumbers
    .filter((ph) => ph.verification.status === 'verified')
    .filter((ph) => !ph.reservedForSecondFactor)

  // Reserve a phone number for MFA
  const reservePhoneForMfa = async (phone: PhoneNumberResource) => {
    // Set the phone number as reserved for MFA
    await setReservedForSecondFactor(phone)
    // Refresh the user information to reflect changes
    await user.reload()
  }

  if (availableForMfaPhones.length === 0) {
    return <p>There are currently no verified phone numbers available to be reserved for MFA.</p>
  }

  return (
    <>
      <h2>Phone numbers that are not reserved for MFA</h2>

      <ul>
        {availableForMfaPhones.map((phone) => {
          return (
            <li key={phone.id} style={{ display: 'flex', gap: '10px' }}>
              <p>{phone.phoneNumber}</p>
              <div>
                <button onClick={() => reservePhoneForMfa(phone)}>Use for MFA</button>
              </div>
              <div>
                <button onClick={() => destroyPhone()}>Remove from account</button>
              </div>
            </li>
          )
        })}
      </ul>
    </>
  )
}

// Generate and display backup codes
function GenerateBackupCodes() {
  const { user } = useUser()
  const [backupCodes, setBackupCodes] = React.useState<BackupCodeResource | undefined>(undefined)
  const createBackupCode = useReverification(() => user?.createBackupCode())

  const [loading, setLoading] = React.useState(false)

  React.useEffect(() => {
    if (backupCodes) {
      return
    }

    setLoading(true)
    void createBackupCode()
      .then((backupCode: BackupCodeResource) => {
        setBackupCodes(backupCode)
        setLoading(false)
      })
      .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))
        setLoading(false)
      })
  }, [])

  if (loading) {
    return <p>Loading...</p>
  }

  if (!backupCodes) {
    return <p>There was a problem generating backup codes</p>
  }

  return (
    <ol>
      {backupCodes.codes.map((code, index) => (
        <li key={index}>{code}</li>
      ))}
    </ol>
  )
}

export default function ManageSMSMFA() {
  const [showBackupCodes, setShowBackupCodes] = React.useState(false)

  const { isLoaded, isSignedIn, user } = useUser()

  if (!isLoaded) {
    // Handle loading state
    return null
  }

  if (!isSignedIn) {
    // Handle signed out state
    return <p>You must be signed in to access this page</p>
  }

  return (
    <>
      <h1>User MFA Settings</h1>

      {/* Manage SMS MFA */}
      <ManageMfaPhoneNumbers />
      <ManageAvailablePhoneNumbers />
      <Link href="/account/add-phone">Add a new phone number</Link>

      {/* Manage backup codes */}
      {user.twoFactorEnabled && (
        <div>
          <p>
            Generate new backup codes? -{' '}
            <button onClick={() => setShowBackupCodes(true)}>Generate</button>
          </p>
        </div>
      )}
      {showBackupCodes && (
        <>
          <GenerateBackupCodes />
          <button onClick={() => setShowBackupCodes(false)}>Done</button>
        </>
      )}
    </>
  )
}
app/(account)/manage-mfa/page.tsx
import { ThemedText } from '@/components/themed-text'
import { ThemedView } from '@/components/themed-view'
import { useUser } from '@clerk/clerk-expo'
import { BackupCodeResource, PhoneNumberResource } from '@clerk/types'
import { Redirect, useRouter } from 'expo-router'
import * as React from 'react'
import { Pressable, StyleSheet, View } from 'react-native'

// Display phone numbers reserved for MFA
const ManageMfaPhoneNumbers = () => {
  const { isSignedIn, user } = useUser()

  // Handle signed-out state
  if (!isSignedIn) return <Redirect href="/sign-in" />

  // Check if any phone numbers are reserved for MFA
  const mfaPhones = user.phoneNumbers
    .filter((ph) => ph.verification.status === 'verified')
    .filter((ph) => ph.reservedForSecondFactor)
    .sort((ph: PhoneNumberResource) => (ph.defaultSecondFactor ? -1 : 1))

  if (mfaPhones.length === 0) {
    return (
      <ThemedText style={styles.infoText}>
        There are currently no phone numbers reserved for MFA.
      </ThemedText>
    )
  }

  return (
    <View style={styles.section}>
      <ThemedText type="subtitle" style={styles.sectionTitle}>
        Phone numbers reserved for MFA
      </ThemedText>
      <View style={styles.list}>
        {mfaPhones.map((phone) => {
          return (
            <View key={phone.id} style={styles.listItem}>
              <ThemedText style={styles.phoneNumber}>
                {phone.phoneNumber} {phone.defaultSecondFactor && '(Default)'}
              </ThemedText>
              <View style={styles.buttonGroup}>
                <Pressable
                  style={({ pressed }) => [styles.smallButton, pressed && styles.buttonPressed]}
                  onPress={() => phone.setReservedForSecondFactor({ reserved: false })}
                >
                  <ThemedText style={styles.smallButtonText}>Disable for MFA</ThemedText>
                </Pressable>

                {!phone.defaultSecondFactor && (
                  <Pressable
                    style={({ pressed }) => [styles.smallButton, pressed && styles.buttonPressed]}
                    onPress={() => phone.makeDefaultSecondFactor()}
                  >
                    <ThemedText style={styles.smallButtonText}>Make default</ThemedText>
                  </Pressable>
                )}

                <Pressable
                  style={({ pressed }) => [
                    styles.smallButton,
                    styles.dangerButton,
                    pressed && styles.buttonPressed,
                  ]}
                  onPress={() => phone.destroy()}
                >
                  <ThemedText style={styles.dangerButtonText}>Remove</ThemedText>
                </Pressable>
              </View>
            </View>
          )
        })}
      </View>
    </View>
  )
}

// Display phone numbers that are not reserved for MFA
const ManageAvailablePhoneNumbers = () => {
  const { isSignedIn, user } = useUser()

  // Handle signed-out state
  if (!isSignedIn) return <Redirect href="/sign-in" />

  // Check if any phone numbers aren't reserved for MFA
  const availableForMfaPhones = user.phoneNumbers
    .filter((ph) => ph.verification.status === 'verified')
    .filter((ph) => !ph.reservedForSecondFactor)

  // Reserve a phone number for MFA
  const reservePhoneForMfa = async (phone: PhoneNumberResource) => {
    // Set the phone number as reserved for MFA
    await phone.setReservedForSecondFactor({ reserved: true })
    // Refresh the user information to reflect changes
    await user.reload()
  }

  // Remove a phone number
  const removePhone = async (phone: PhoneNumberResource) => {
    await phone.destroy()
    await user.reload()
  }

  if (availableForMfaPhones.length === 0) {
    return (
      <ThemedText style={styles.infoText}>
        There are currently no verified phone numbers available to be reserved for MFA.
      </ThemedText>
    )
  }

  return (
    <View style={styles.section}>
      <ThemedText type="subtitle" style={styles.sectionTitle}>
        Phone numbers that are not reserved for MFA
      </ThemedText>
      <View style={styles.list}>
        {availableForMfaPhones.map((phone) => {
          return (
            <View key={phone.id} style={styles.listItem}>
              <ThemedText style={styles.phoneNumber}>{phone.phoneNumber}</ThemedText>
              <View style={styles.buttonGroup}>
                <Pressable
                  style={({ pressed }) => [styles.smallButton, pressed && styles.buttonPressed]}
                  onPress={() => reservePhoneForMfa(phone)}
                >
                  <ThemedText style={styles.smallButtonText}>Use for MFA</ThemedText>
                </Pressable>
                <Pressable
                  style={({ pressed }) => [
                    styles.smallButton,
                    styles.dangerButton,
                    pressed && styles.buttonPressed,
                  ]}
                  onPress={() => removePhone(phone)}
                >
                  <ThemedText style={styles.dangerButtonText}>Remove</ThemedText>
                </Pressable>
              </View>
            </View>
          )
        })}
      </View>
    </View>
  )
}

// Generate and display backup codes
function GenerateBackupCodes() {
  const { user } = useUser()
  const [backupCodes, setBackupCodes] = React.useState<BackupCodeResource | undefined>(undefined)
  const [loading, setLoading] = React.useState(false)

  React.useEffect(() => {
    if (backupCodes) return

    setLoading(true)
    user
      ?.createBackupCode()
      .then((backupCode: BackupCodeResource) => {
        setBackupCodes(backupCode)
        setLoading(false)
      })
      .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))
        setLoading(false)
      })
  }, [])

  // Handle loading state
  if (loading) return <ThemedText>Loading...</ThemedText>

  if (!backupCodes)
    return (
      <ThemedText style={styles.errorText}>There was a problem generating backup codes</ThemedText>
    )

  return (
    <View style={styles.codeList}>
      {backupCodes.codes.map((code, index) => (
        <ThemedText key={index} style={styles.code}>
          {index + 1}. {code}
        </ThemedText>
      ))}
    </View>
  )
}

export default function ManageSMSMFA() {
  const [showBackupCodes, setShowBackupCodes] = React.useState(false)
  const router = useRouter()

  const { isLoaded, isSignedIn, user } = useUser()

  // Handle loading state
  if (!isLoaded) {
    return (
      <ThemedView style={styles.container}>
        <ThemedText>Loading...</ThemedText>
      </ThemedView>
    )
  }

  // Handle signed-out state
  if (!isSignedIn) return <Redirect href="/sign-in" />

  return (
    <ThemedView style={styles.container}>
      <ThemedText type="title" style={styles.title}>
        User MFA Settings
      </ThemedText>

      <ManageMfaPhoneNumbers />
      <ManageAvailablePhoneNumbers />

      <Pressable
        style={({ pressed }) => [styles.button, pressed && styles.buttonPressed]}
        onPress={() => router.push('/add-phone' as any)}
      >
        <ThemedText style={styles.buttonText}>Add a new phone number</ThemedText>
      </Pressable>

      {user.twoFactorEnabled && (
        <View style={styles.section}>
          <ThemedText style={styles.infoText}>Generate new backup codes?</ThemedText>
          <Pressable
            style={({ pressed }) => [styles.button, pressed && styles.buttonPressed]}
            onPress={() => setShowBackupCodes(true)}
          >
            <ThemedText style={styles.buttonText}>Generate</ThemedText>
          </Pressable>
        </View>
      )}

      {showBackupCodes && (
        <View style={styles.section}>
          <GenerateBackupCodes />
          <Pressable
            style={({ pressed }) => [styles.button, pressed && styles.buttonPressed]}
            onPress={() => setShowBackupCodes(false)}
          >
            <ThemedText style={styles.buttonText}>Done</ThemedText>
          </Pressable>
        </View>
      )}
    </ThemedView>
  )
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    padding: 20,
    gap: 16,
  },
  title: {
    marginBottom: 8,
  },
  section: {
    gap: 12,
    marginBottom: 16,
  },
  sectionTitle: {
    fontWeight: '600',
    fontSize: 16,
    marginBottom: 8,
  },
  list: {
    gap: 12,
  },
  listItem: {
    padding: 12,
    backgroundColor: '#f5f5f5',
    borderRadius: 8,
    gap: 8,
  },
  phoneNumber: {
    fontSize: 16,
    fontWeight: '500',
  },
  buttonGroup: {
    flexDirection: 'row',
    flexWrap: 'wrap',
    gap: 8,
  },
  button: {
    backgroundColor: '#0a7ea4',
    paddingVertical: 12,
    paddingHorizontal: 24,
    borderRadius: 8,
    alignItems: 'center',
    marginTop: 8,
  },
  smallButton: {
    backgroundColor: '#0a7ea4',
    paddingVertical: 8,
    paddingHorizontal: 16,
    borderRadius: 6,
    alignItems: 'center',
  },
  dangerButton: {
    backgroundColor: '#c62828',
  },
  buttonPressed: {
    opacity: 0.7,
  },
  buttonText: {
    color: '#fff',
    fontWeight: '600',
  },
  smallButtonText: {
    color: '#fff',
    fontWeight: '600',
    fontSize: 13,
  },
  dangerButtonText: {
    color: '#fff',
    fontWeight: '600',
    fontSize: 13,
  },
  infoText: {
    fontSize: 14,
    opacity: 0.8,
  },
  errorText: {
    color: '#c62828',
    fontSize: 14,
  },
  codeList: {
    padding: 12,
    backgroundColor: '#f5f5f5',
    borderRadius: 8,
    gap: 8,
  },
  code: {
    fontFamily: 'monospace',
    fontSize: 14,
  },
})

Examples for this SDK aren't available yet. For now, try switching to a supported SDK, such as Next.js, and converting the code to fit your SDK.

Warning

Phone numbers must be in E.164 format.

This example is written for Next.js App Router but it can be adapted for any React-based framework, such as React Router or Tanstack React Start.

  1. 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 useUser() hook is used to get the User object.
  2. 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.
  3. If the createPhoneNumber() function is successful, a new PhoneNumber object is created and stored in User.phoneNumbers.
  4. Uses the prepareVerification() method on the newly created PhoneNumber object to send a verification code to the user.
  5. Uses the attemptVerification() method on the same PhoneNumber object with the verification code provided by the user to verify the phone number.
app/account/add-phone/page.tsx
'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 }),
  )

  // Handle loading state
  if (!isLoaded) <p>Loading...</p>

  // Handle signed-out state
  if (!isSignedIn) <p>You must be signed 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 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 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>
    </>
  )
}
app/(account)/add-phone/page.tsx
import { ThemedText } from '@/components/themed-text'
import { ThemedView } from '@/components/themed-view'
import { useUser } from '@clerk/clerk-expo'
import { PhoneNumberResource } from '@clerk/types'
import { Redirect } from 'expo-router'
import * as React from 'react'
import { Pressable, StyleSheet, TextInput } from 'react-native'

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

  // Handle loading state
  if (!isLoaded) {
    return (
      <ThemedView style={styles.container}>
        <ThemedText>Loading...</ThemedText>
      </ThemedView>
    )
  }

  // Handle signed-out state
  if (!isSignedIn) {
    return <Redirect href="/sign-in" />
  }

  // Handle addition of the phone number
  const handleSubmit = async () => {
    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
      await phoneNumber?.prepareVerification()

      // Set to true to display second form
      // and capture the 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 () => {
    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 (
      <ThemedView style={styles.container}>
        <ThemedText type="title" style={styles.title}>
          Phone added
        </ThemedText>
      </ThemedView>
    )
  }

  // Display the verification form to capture the code
  if (isVerifying) {
    return (
      <ThemedView style={styles.container}>
        <ThemedText type="title" style={styles.title}>
          Verify phone
        </ThemedText>
        <ThemedText style={styles.label}>Enter code</ThemedText>
        <TextInput
          style={styles.input}
          value={code}
          placeholder="Enter code"
          placeholderTextColor="#666666"
          onChangeText={setCode}
          keyboardType="numeric"
        />
        <Pressable
          style={({ pressed }) => [
            styles.button,
            !code && styles.buttonDisabled,
            pressed && styles.buttonPressed,
          ]}
          onPress={verifyCode}
          disabled={!code}
        >
          <ThemedText style={styles.buttonText}>Verify</ThemedText>
        </Pressable>
      </ThemedView>
    )
  }

  // Display the initial form to capture the phone number
  return (
    <ThemedView style={styles.container}>
      <ThemedText type="title" style={styles.title}>
        Add phone
      </ThemedText>
      <ThemedText style={styles.label}>Enter phone number</ThemedText>
      <TextInput
        style={styles.input}
        value={phone}
        placeholder="e.g +1234567890"
        placeholderTextColor="#666666"
        onChangeText={setPhone}
        keyboardType="phone-pad"
      />
      <Pressable
        style={({ pressed }) => [
          styles.button,
          !phone && styles.buttonDisabled,
          pressed && styles.buttonPressed,
        ]}
        onPress={handleSubmit}
        disabled={!phone}
      >
        <ThemedText style={styles.buttonText}>Continue</ThemedText>
      </Pressable>
    </ThemedView>
  )
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    padding: 20,
    gap: 12,
  },
  title: {
    marginBottom: 8,
  },
  label: {
    fontWeight: '600',
    fontSize: 14,
  },
  input: {
    borderWidth: 1,
    borderColor: '#ccc',
    borderRadius: 8,
    padding: 12,
    fontSize: 16,
    backgroundColor: '#fff',
  },
  button: {
    backgroundColor: '#0a7ea4',
    paddingVertical: 12,
    paddingHorizontal: 24,
    borderRadius: 8,
    alignItems: 'center',
    marginTop: 8,
  },
  buttonPressed: {
    opacity: 0.7,
  },
  buttonDisabled: {
    opacity: 0.5,
  },
  buttonText: {
    color: '#fff',
    fontWeight: '600',
  },
})
AddPhoneView.swift
  import SwiftUI
  import ClerkKit

  struct AddPhoneView: View {
    @State private var phone = ""
    @State private var code = ""
    @State private var isVerifying = false
    @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 }

        // Create the phone number
        let phoneNumber = try await user.createPhoneNumber(phone)

        // Send the user an SMS with the verification code
        self.newPhoneNumber = try await phoneNumber.sendCode()

        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 provided code matches the code sent to the user
        self.newPhoneNumber = try await newPhoneNumber.verifyCode(code)

        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)
      }
    }
  }
AddPhoneViewModel.kt
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.errorMessage
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.errorMessage}",
          )
        }
    }
  }

  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.errorMessage}")
        }
    }
  }

  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
  }
}
AddPhoneActivity.kt
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) }
  }
}

Examples for this SDK aren't available yet. For now, try switching to a supported SDK, such as Next.js, and converting the code to fit your SDK.

Enable multi-factor authentication

For your users to be able to enable MFA for their account, you need to enable MFA for your application.

  1. In the Clerk Dashboard, navigate to the Multi-factor page.
  2. For the purpose of this guide, toggle on both the Authenticator application and Backup codes strategies.
  3. Select Save.

Warning

If you're using Duo as an authenticator app, please note that Duo generates TOTP codes differently than other authenticator apps. Duo allows a code to be valid for 30 seconds from the moment it is first displayed, which may cause frequent invalid_code errors if the code is not entered promptly. More information can be found in Duo's Help Center.

This example is written for Next.js App Router but it can be adapted for any React-based framework, such as React Router or Tanstack React Start.

This example consists of two pages:

  • The main page where users can manage their MFA settings
  • The page where users can add TOTP MFA.

Use the following tabs to view the code necessary for each page.

app/account/manage-mfa/page.tsx
'use client'

import * as React from 'react'
import { useUser, useReverification } from '@clerk/nextjs'
import Link from 'next/link'
import { BackupCodeResource } from '@clerk/types'

// If TOTP is enabled, provide the option to disable it
const TotpEnabled = () => {
  const { user } = useUser()
  const disableTOTP = useReverification(() => user?.disableTOTP())

  return (
    <p>
      TOTP via authentication app enabled - <button onClick={() => disableTOTP()}>Remove</button>
    </p>
  )
}

// If TOTP is disabled, provide the option to enable it
const TotpDisabled = () => {
  return (
    <p>
      Add TOTP via authentication app -{' '}
      <Link href="/account/manage-mfa/add">
        <button>Add</button>
      </Link>
    </p>
  )
}

// Generate and display backup codes
export function GenerateBackupCodes() {
  const { user } = useUser()
  const [backupCodes, setBackupCodes] = React.useState<BackupCodeResource | undefined>(undefined)
  const createBackupCode = useReverification(() => user?.createBackupCode())

  const [loading, setLoading] = React.useState(false)

  React.useEffect(() => {
    if (backupCodes) return

    setLoading(true)
    void createBackupCode()
      .then((backupCode: BackupCodeResource | undefined) => {
        setBackupCodes(backupCode)
        setLoading(false)
      })
      .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))
        setLoading(false)
      })
  }, [])

  // Handle loading state
  if (loading) <p>Loading...</p>

  if (!backupCodes) return <p>There was a problem generating backup codes</p>

  return (
    <ol>
      {backupCodes.codes.map((code, index) => (
        <li key={index}>{code}</li>
      ))}
    </ol>
  )
}

export default function ManageMFA() {
  const { isLoaded, isSignedIn, user } = useUser()
  const [showNewCodes, setShowNewCodes] = React.useState(false)

  // Handle loading state
  if (!isLoaded) <p>Loading...</p>

  // Handle signed-out state
  if (!isSignedIn) <p>You must be signed in to access this page</p>

  return (
    <>
      <h1>User MFA Settings</h1>

      {/* Manage TOTP MFA */}
      {user.totpEnabled ? <TotpEnabled /> : <TotpDisabled />}

      {/* Manage backup codes */}
      {user.backupCodeEnabled && user.twoFactorEnabled && (
        <p>
          Generate new backup codes? -{' '}
          <button onClick={() => setShowNewCodes(true)}>Generate</button>
        </p>
      )}
      {showNewCodes && (
        <>
          <GenerateBackupCodes />
          <button onClick={() => setShowNewCodes(false)}>Done</button>
        </>
      )}
    </>
  )
}
app/account/manage-mfa/add/page.tsx
'use client'

import { useUser, useReverification } from '@clerk/nextjs'
import { TOTPResource } from '@clerk/types'
import Link from 'next/link'
import * as React from 'react'
import { QRCodeSVG } from 'qrcode.react'
import { GenerateBackupCodes } from '../page'

type AddTotpSteps = 'add' | 'verify' | 'backupcodes' | 'success'

type DisplayFormat = 'qr' | 'uri'

function AddTotpScreen({
  setStep,
}: {
  setStep: React.Dispatch<React.SetStateAction<AddTotpSteps>>
}) {
  const { user } = useUser()
  const [totp, setTOTP] = React.useState<TOTPResource | undefined>(undefined)
  const [displayFormat, setDisplayFormat] = React.useState<DisplayFormat>('qr')
  const createTOTP = useReverification(() => user?.createTOTP())

  React.useEffect(() => {
    void createTOTP()
      .then((totp: TOTPResource) => {
        setTOTP(totp)
      })
      .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)),
      )
  }, [])

  return (
    <>
      <h1>Add TOTP MFA</h1>

      {totp && displayFormat === 'qr' && (
        <>
          <div>
            <QRCodeSVG value={totp?.uri || ''} size={200} />
          </div>
          <button onClick={() => setDisplayFormat('uri')}>Use URI instead</button>
        </>
      )}
      {totp && displayFormat === 'uri' && (
        <>
          <div>
            <p>{totp.uri}</p>
          </div>
          <button onClick={() => setDisplayFormat('qr')}>Use QR Code instead</button>
        </>
      )}
      <button onClick={() => setStep('add')}>Reset</button>

      <p>Once you have set up your authentication app, verify your code</p>
      <button onClick={() => setStep('verify')}>Verify</button>
    </>
  )
}

function VerifyTotpScreen({
  setStep,
}: {
  setStep: React.Dispatch<React.SetStateAction<AddTotpSteps>>
}) {
  const { user } = useUser()
  const [code, setCode] = React.useState('')

  const verifyTotp = async (e: React.FormEvent) => {
    e.preventDefault()
    try {
      await user?.verifyTOTP({ code })
      setStep('backupcodes')
    } catch (err) {
      console.error(JSON.stringify(err, null, 2))
    }
  }

  return (
    <>
      <h1>Verify TOTP</h1>
      <form onSubmit={(e) => verifyTotp(e)}>
        <label htmlFor="totp-code">Enter the code from your authentication app</label>
        <input type="text" id="totp-code" onChange={(e) => setCode(e.currentTarget.value)} />
        <button type="submit">Verify code</button>
        <button onClick={() => setStep('add')}>Reset</button>
      </form>
    </>
  )
}

function BackupCodeScreen({
  setStep,
}: {
  setStep: React.Dispatch<React.SetStateAction<AddTotpSteps>>
}) {
  return (
    <>
      <h1>Verification was a success!</h1>
      <div>
        <p>
          Save this list of backup codes somewhere safe in case you need to access your account in
          an emergency
        </p>
        <GenerateBackupCodes />
        <button onClick={() => setStep('success')}>Finish</button>
      </div>
    </>
  )
}

function SuccessScreen() {
  return (
    <>
      <h1>Success!</h1>
      <p>You have successfully added TOTP MFA via an authentication application.</p>
    </>
  )
}

export default function AddMFaScreen() {
  const { isLoaded, isSignedIn, user } = useUser()

  const [step, setStep] = React.useState<AddTotpSteps>('add')

  // Handle loading state
  if (!isLoaded) <p>Loading...</p>

  // Handle signed out state
  if (!isSignedIn) <p>You must be signed in to access this page</p>

  return (
    <>
      {step === 'add' && <AddTotpScreen setStep={setStep} />}
      {step === 'verify' && <VerifyTotpScreen setStep={setStep} />}
      {step === 'backupcodes' && <BackupCodeScreen setStep={setStep} />}
      {step === 'success' && <SuccessScreen />}
      <Link href="/account/manage-mfa">Manage MFA</Link>
    </>
  )
}

Before you start

Install expo-checkbox for the UI and react-native-qr-svg for the QR code.

terminal
npm install expo-checkbox react-native-qr-svg
terminal
pnpm add expo-checkbox react-native-qr-svg
terminal
yarn add expo-checkbox react-native-qr-svg
terminal
bun add expo-checkbox react-native-qr-svg

Build the flow

To allow users to configure their MFA settings, you'll create a basic dashboard.

The following example consists of three pages:

  • The layout page that checks if the user is signed in
  • The page where users can manage their account, including their MFA settings
  • The page where users can add TOTP MFA

Use the following tabs to view the code necessary for each page.

  1. Create the (account) route group. This groups your account page and the "Add TOTP MFA" page.
  2. Create a _layout.tsx file with the following code. The useAuth() hook is used to check if the user is signed in. If the user isn't signed in, they'll be redirected to the sign-in page.
app/(account)/_layout.tsx
import { Redirect, Stack } from 'expo-router'
import { useAuth } from '@clerk/clerk-expo'

export default function AuthenticatedLayout() {
  const { isSignedIn } = useAuth()

  if (!isSignedIn) {
    return <Redirect href={'/sign-in'} />
  }

  return <Stack />
}

In the (account) group, create an index.tsx file with the following code. This page shows users whether or not MFA is enabled, and allows them to add MFA with an authenticator app.

app/(account)/index.tsx
import { ThemedText } from '@/components/themed-text'
import { ThemedView } from '@/components/themed-view'
import { useUser } from '@clerk/clerk-expo'
import { BackupCodeResource } from '@clerk/types'
import { Redirect, useRouter } from 'expo-router'
import React from 'react'
import { FlatList, Pressable, StyleSheet, View } from 'react-native'

export default function ManageTOTPMfa() {
  const { isLoaded, isSignedIn, user } = useUser()
  const router = useRouter()

  const [backupCodes, setBackupCodes] = React.useState<BackupCodeResource | undefined>(undefined)
  const [loading, setLoading] = React.useState(false)

  if (!isLoaded) {
    return (
      <ThemedView style={styles.container}>
        <ThemedText>Loading...</ThemedText>
      </ThemedView>
    )
  }

  if (!isSignedIn) {
    return <Redirect href="/sign-in" />
  }

  const generateBackupCodes = () => {
    setLoading(true)
    user
      ?.createBackupCode()
      .then((backupCodes: BackupCodeResource) => {
        setBackupCodes(backupCodes)
        setLoading(false)
      })
      .catch((error) => {
        console.log('Error:', error)
        setLoading(false)
      })
  }

  const disableTOTP = async () => {
    await user.disableTOTP()
  }

  const MFAEnabled = () => {
    return (
      <View style={styles.mfaRow}>
        <ThemedText style={styles.infoText}>TOTP via authentication app enabled</ThemedText>
        <Pressable
          style={({ pressed }) => [
            styles.smallButton,
            styles.dangerButton,
            pressed && styles.buttonPressed,
          ]}
          onPress={() => disableTOTP()}
        >
          <ThemedText style={styles.smallButtonText}>Remove</ThemedText>
        </Pressable>
      </View>
    )
  }

  const MFADisabled = () => {
    return (
      <View style={styles.mfaRow}>
        <ThemedText style={styles.infoText}>Add TOTP via authentication app</ThemedText>
        <Pressable
          style={({ pressed }) => [styles.smallButton, pressed && styles.buttonPressed]}
          onPress={() => router.push('/add-mfa' as any)}
        >
          <ThemedText style={styles.smallButtonText}>Add</ThemedText>
        </Pressable>
      </View>
    )
  }

  return (
    <ThemedView style={styles.container}>
      <ThemedText type="title" style={styles.title}>
        Current MFA Settings
      </ThemedText>

      <View style={styles.section}>
        <ThemedText type="subtitle" style={styles.sectionTitle}>
          Authenticator App
        </ThemedText>

        {user.totpEnabled ? <MFAEnabled /> : <MFADisabled />}
      </View>

      {user.backupCodeEnabled && (
        <View style={styles.section}>
          <ThemedText type="subtitle" style={styles.sectionTitle}>
            Backup Codes
          </ThemedText>

          {loading && <ThemedText>Loading...</ThemedText>}

          {backupCodes && !loading && (
            <View style={styles.codeList}>
              <FlatList
                data={backupCodes.codes}
                renderItem={({ item, index }) => (
                  <ThemedText key={index} style={styles.code}>
                    {index + 1}. {item}
                  </ThemedText>
                )}
                keyExtractor={(item) => item}
              />
            </View>
          )}

          <Pressable
            style={({ pressed }) => [styles.button, pressed && styles.buttonPressed]}
            onPress={() => generateBackupCodes()}
          >
            <ThemedText style={styles.buttonText}>Regenerate Codes</ThemedText>
          </Pressable>
        </View>
      )}
    </ThemedView>
  )
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    padding: 20,
    gap: 16,
  },
  title: {
    marginBottom: 8,
  },
  section: {
    gap: 12,
    marginBottom: 16,
  },
  sectionTitle: {
    fontWeight: '600',
    fontSize: 16,
    marginBottom: 8,
  },
  mfaRow: {
    flexDirection: 'row',
    alignItems: 'center',
    justifyContent: 'space-between',
    padding: 12,
    backgroundColor: '#f5f5f5',
    borderRadius: 8,
    gap: 12,
  },
  infoText: {
    fontSize: 14,
    flex: 1,
  },
  button: {
    backgroundColor: '#0a7ea4',
    paddingVertical: 12,
    paddingHorizontal: 24,
    borderRadius: 8,
    alignItems: 'center',
    marginTop: 8,
  },
  smallButton: {
    backgroundColor: '#0a7ea4',
    paddingVertical: 8,
    paddingHorizontal: 16,
    borderRadius: 6,
    alignItems: 'center',
  },
  dangerButton: {
    backgroundColor: '#c62828',
  },
  buttonPressed: {
    opacity: 0.7,
  },
  buttonText: {
    color: '#fff',
    fontWeight: '600',
  },
  smallButtonText: {
    color: '#fff',
    fontWeight: '600',
    fontSize: 13,
  },
  codeList: {
    padding: 12,
    backgroundColor: '#f5f5f5',
    borderRadius: 8,
    gap: 8,
  },
  code: {
    fontFamily: 'monospace',
    fontSize: 14,
    paddingVertical: 4,
  },
})

In the (account) group, create a manage-mfa.tsx file with the following code. This page adds the functionality for generating the QR code and backup codes.

app/(account)/manage-mfa.tsx
import { ThemedText } from '@/components/themed-text'
import { ThemedView } from '@/components/themed-view'
import { useUser } from '@clerk/clerk-expo'
import { BackupCodeResource, TOTPResource } from '@clerk/types'
import { Redirect, useRouter } from 'expo-router'
import React from 'react'
import { FlatList, Pressable, ScrollView, StyleSheet, TextInput, View } from 'react-native'
import { QrCodeSvg } from 'react-native-qr-svg'

type AddTotpSteps = 'add' | 'verify' | 'backupcodes' | 'success'
type DisplayFormat = 'qr' | 'uri'

function AddTOTPMfa({ setStep }: { setStep: React.Dispatch<React.SetStateAction<AddTotpSteps>> }) {
  const [totp, setTotp] = React.useState<TOTPResource | undefined>(undefined)
  const [displayFormat, setDisplayFormat] = React.useState<DisplayFormat>('qr')
  const { user } = useUser()

  React.useEffect(() => {
    user
      ?.createTOTP()
      .then((totp: TOTPResource) => setTotp(totp))
      .catch((err) => console.error(JSON.stringify(err, null, 2)))
  }, [])

  return (
    <View style={styles.section}>
      <ThemedText type="title" style={styles.title}>
        Add TOTP MFA
      </ThemedText>

      {totp && displayFormat === 'qr' && (
        <View style={styles.qrContainer}>
          <View style={styles.qrCode}>
            <QrCodeSvg value={totp?.uri || ''} frameSize={200} />
          </View>
          <Pressable
            style={({ pressed }) => [styles.smallButton, pressed && styles.buttonPressed]}
            onPress={() => setDisplayFormat('uri')}
          >
            <ThemedText style={styles.smallButtonText}>Use URI instead</ThemedText>
          </Pressable>
        </View>
      )}

      {totp && displayFormat === 'uri' && (
        <View style={styles.uriContainer}>
          <View style={styles.uriBox}>
            <ThemedText style={styles.uriText}>{totp.uri}</ThemedText>
          </View>
          <Pressable
            style={({ pressed }) => [styles.smallButton, pressed && styles.buttonPressed]}
            onPress={() => setDisplayFormat('qr')}
          >
            <ThemedText style={styles.smallButtonText}>Use QR Code instead</ThemedText>
          </Pressable>
        </View>
      )}

      <View style={styles.buttonGroup}>
        <Pressable
          style={({ pressed }) => [styles.button, pressed && styles.buttonPressed]}
          onPress={() => setStep('verify')}
        >
          <ThemedText style={styles.buttonText}>Verify</ThemedText>
        </Pressable>

        <Pressable
          style={({ pressed }) => [
            styles.button,
            styles.secondaryButton,
            pressed && styles.buttonPressed,
          ]}
          onPress={() => setStep('add')}
        >
          <ThemedText style={styles.secondaryButtonText}>Reset</ThemedText>
        </Pressable>
      </View>
    </View>
  )
}

function VerifyMFA({ setStep }: { setStep: React.Dispatch<React.SetStateAction<AddTotpSteps>> }) {
  const [code, setCode] = React.useState('')
  const { user } = useUser()

  const verifyTotp = async () => {
    await user
      ?.verifyTOTP({ code })
      .then(() => setStep('backupcodes'))
      .catch((err) => console.error(JSON.stringify(err, null, 2)))
  }

  return (
    <View style={styles.section}>
      <ThemedText type="title" style={styles.title}>
        Verify MFA
      </ThemedText>

      <ThemedText style={styles.label}>Enter the code from your authenticator app</ThemedText>
      <TextInput
        style={styles.input}
        value={code}
        placeholder="Enter code"
        placeholderTextColor="#666666"
        onChangeText={setCode}
        keyboardType="numeric"
      />

      <View style={styles.buttonGroup}>
        <Pressable
          style={({ pressed }) => [
            styles.button,
            !code && styles.buttonDisabled,
            pressed && styles.buttonPressed,
          ]}
          onPress={verifyTotp}
          disabled={!code}
        >
          <ThemedText style={styles.buttonText}>Verify Code</ThemedText>
        </Pressable>

        <Pressable
          style={({ pressed }) => [
            styles.button,
            styles.secondaryButton,
            pressed && styles.buttonPressed,
          ]}
          onPress={() => setStep('add')}
        >
          <ThemedText style={styles.secondaryButtonText}>Reset</ThemedText>
        </Pressable>
      </View>
    </View>
  )
}

function BackupCodes({ setStep }: { setStep: React.Dispatch<React.SetStateAction<AddTotpSteps>> }) {
  const { user } = useUser()
  const [backupCode, setBackupCode] = React.useState<BackupCodeResource | undefined>(undefined)

  React.useEffect(() => {
    if (backupCode) return

    user
      ?.createBackupCode()
      .then((backupCode: BackupCodeResource) => setBackupCode(backupCode))
      .catch((err) => console.error(JSON.stringify(err, null, 2)))
  }, [])

  return (
    <View style={styles.section}>
      <ThemedText type="title" style={styles.title}>
        Verification was a success!
      </ThemedText>

      {backupCode && (
        <View style={styles.backupCodesContainer}>
          <ThemedText style={styles.description}>
            Save this list of backup codes somewhere safe in case you need to access your account in
            an emergency
          </ThemedText>

          <View style={styles.codeList}>
            <FlatList
              data={backupCode.codes.map((code, index) => ({
                key: code,
                index: index + 1,
              }))}
              renderItem={({ item }) => (
                <ThemedText style={styles.code}>
                  {item.index}. {item.key}
                </ThemedText>
              )}
              keyExtractor={(item) => item.key}
            />
          </View>

          <Pressable
            style={({ pressed }) => [styles.button, pressed && styles.buttonPressed]}
            onPress={() => setStep('success')}
          >
            <ThemedText style={styles.buttonText}>Finish</ThemedText>
          </Pressable>
        </View>
      )}
    </View>
  )
}

function Success() {
  const router = useRouter()

  return (
    <View style={styles.section}>
      <ThemedText type="title" style={styles.title}>
        Success!
      </ThemedText>

      <ThemedText style={styles.description}>
        You successfully added TOTP MFA via an authentication application
      </ThemedText>

      <Pressable
        style={({ pressed }) => [styles.button, pressed && styles.buttonPressed]}
        onPress={() => router.push('/account' as any)}
      >
        <ThemedText style={styles.buttonText}>Go to Account Settings</ThemedText>
      </Pressable>
    </View>
  )
}

export default function ManageTOTPMFA() {
  const [step, setStep] = React.useState<AddTotpSteps>('add')
  const { isLoaded, isSignedIn } = useUser()

  if (!isLoaded) {
    return (
      <ThemedView style={styles.container}>
        <ThemedText>Loading...</ThemedText>
      </ThemedView>
    )
  }

  if (!isSignedIn) {
    return <Redirect href="/sign-in" />
  }

  return (
    <ScrollView>
      <ThemedView style={styles.container}>
        {step === 'add' && <AddTOTPMfa setStep={setStep} />}
        {step === 'verify' && <VerifyMFA setStep={setStep} />}
        {step === 'backupcodes' && <BackupCodes setStep={setStep} />}
        {step === 'success' && <Success />}
      </ThemedView>
    </ScrollView>
  )
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    padding: 20,
  },
  section: {
    gap: 16,
  },
  title: {
    marginBottom: 8,
  },
  label: {
    fontWeight: '600',
    fontSize: 14,
    marginBottom: 4,
  },
  description: {
    fontSize: 14,
    opacity: 0.8,
    marginBottom: 16,
  },
  qrContainer: {
    alignItems: 'center',
    gap: 16,
    marginVertical: 16,
  },
  qrCode: {
    padding: 16,
    backgroundColor: '#fff',
    borderRadius: 12,
    shadowColor: '#000',
    shadowOffset: { width: 0, height: 2 },
    shadowOpacity: 0.1,
    shadowRadius: 4,
    elevation: 3,
  },
  uriContainer: {
    gap: 12,
    marginVertical: 16,
  },
  uriBox: {
    padding: 16,
    backgroundColor: '#f5f5f5',
    borderRadius: 8,
    borderWidth: 1,
    borderColor: '#ccc',
  },
  uriText: {
    fontFamily: 'monospace',
    fontSize: 12,
  },
  input: {
    borderWidth: 1,
    borderColor: '#ccc',
    borderRadius: 8,
    padding: 12,
    fontSize: 16,
    backgroundColor: '#fff',
    marginBottom: 8,
  },
  buttonGroup: {
    gap: 12,
    marginTop: 8,
  },
  button: {
    backgroundColor: '#0a7ea4',
    paddingVertical: 12,
    paddingHorizontal: 24,
    borderRadius: 8,
    alignItems: 'center',
  },
  smallButton: {
    backgroundColor: '#0a7ea4',
    paddingVertical: 8,
    paddingHorizontal: 16,
    borderRadius: 6,
    alignItems: 'center',
  },
  secondaryButton: {
    backgroundColor: 'transparent',
    borderWidth: 1,
    borderColor: '#0a7ea4',
  },
  buttonPressed: {
    opacity: 0.7,
  },
  buttonDisabled: {
    opacity: 0.5,
  },
  buttonText: {
    color: '#fff',
    fontWeight: '600',
  },
  smallButtonText: {
    color: '#fff',
    fontWeight: '600',
    fontSize: 13,
  },
  secondaryButtonText: {
    color: '#0a7ea4',
    fontWeight: '600',
  },
  backupCodesContainer: {
    gap: 16,
  },
  codeList: {
    padding: 16,
    backgroundColor: '#f5f5f5',
    borderRadius: 8,
    maxHeight: 300,
  },
  code: {
    fontFamily: 'monospace',
    fontSize: 14,
    paddingVertical: 4,
  },
})

Examples for this SDK aren't available yet. For now, try switching to a supported SDK, such as Next.js, and converting the code to fit your SDK.

Feedback

What did you think of this content?

Last updated on