Build a custom flow for managing multi-factor authentication
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.
- In the Clerk Dashboard, navigate to the Multi-factor page.
- For the purpose of this guide, toggle on both the SMS verification code and Backup codes strategies.
- 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.
'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>
</>
)}
</>
)
}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.
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.
- Every user has a
Userobject that represents their account. TheUserobject 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 newPhoneNumberobject 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 }),
)
// 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>
</>
)
}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',
},
}) 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)
}
}
}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
}
}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.
- In the Clerk Dashboard, navigate to the Multi-factor page.
- For the purpose of this guide, toggle on both the Authenticator application and Backup codes strategies.
- Select Save.
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.
'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>
</>
)}
</>
)
}'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.
npm install expo-checkbox react-native-qr-svgpnpm add expo-checkbox react-native-qr-svgyarn add expo-checkbox react-native-qr-svgbun add expo-checkbox react-native-qr-svgBuild 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.
- Create the
(account)route group. This groups your account page and the "Add TOTP MFA" page. - Create a
_layout.tsxfile 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.
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.
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.
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
Last updated on