Registering the listener
- React
- React Native
- Swift
To set up this configuration, use Privy’s
useRegisterMfaListener hook. As a parameter, you must pass a JSON object with an onMfaRequired callback.onMfaRequired callback
Privy will invoke theonMfaRequired callback you set whenever the user is required to complete MFA to use the embedded wallet. When this occurs, any use of the embedded wallet will be “paused” until the user has successfully completed MFA with Privy.In this callback, you should invoke your app’s logic for guiding through completing MFA (done via the useMfa hook). Within this callback, you can also access an methods parameter that contains a list of available MFA methods that the user has enrolled in ('sms' and/or 'totp' and/or 'passkey').MFAProvider.tsx
Report incorrect code
Copy
Ask AI
import {useRegisterMfaListener, MfaMethod} from '@privy-io/react-auth';
import {MFAModal} from '../components/MFAModal';
export const MFAProvider = ({children}: {children: React.ReactNode}) => {
const [isMfaModalOpen, setIsMfaModelOpen] = useState(false);
const [mfaMethods, setMfaMethods] = useState<MfaMethod[]>([]);
useRegisterMfaListener({
// Privy will invoke this whenever the user is required to complete MFA
onMfaRequired: (methods) => {
// Update app's state with the list of available MFA methods for the user
setMfaMethods(methods);
// Open MFA modal to allow user to complete MFA
setIsMfaModalOpen(true);
},
});
return (
<div>
{/* This `MFAModal` component includes all logic for completing the MFA flow with Privy's `useMfa` hook */}
<MFAModal isOpen={isMfaModalOpen} setIsOpen={setIsMfaModalOpen} mfaMethods={mfaMethods} />
{children}
</div>
);
};
In order for Privy to invoke your app’s MFA flow, the component that calls Privy’s
useRegisterMfaListener hook must be mounted whenever the user’s embedded wallet requires that they complete MFA.We recommend that you render this component near the root of your application, so that it is always rendered whenever the embedded wallet may be used.To set up this configuration, use Privy’s
useRegisterMfaListener hook:MFAProvider.tsx
Report incorrect code
Copy
Ask AI
import {useRegisterMfaListener, MfaMethod} from '@privy-io/expo';
import {MFAModal} from '../components/MFAModal';
export const MFAProvider = ({children}: {children: React.ReactNode}) => {
const [isMfaModalOpen, setIsMfaModelOpen] = useState(false);
const [mfaMethods, setMfaMethods] = useState<MfaMethod[]>([]);
useRegisterMfaListener({
// Privy will invoke this whenever the user is required to complete MFA
onMfaRequired: (methods) => {
// Update app's state with the list of available MFA methods for the user
setMfaMethods(methods);
// Open MFA modal to allow user to complete MFA
setIsMfaModalOpen(true);
},
});
return (
<View>
{/* This `MFAModal` component includes all logic for completing the MFA flow with Privy's `useMfa` hook */}
<MFAModal isOpen={isMfaModalOpen} setIsOpen={setIsMfaModalOpen} mfaMethods={mfaMethods} />
{children}
</View>
);
};
In order for Privy to invoke your app’s MFA flow, the component that calls Privy’s
useRegisterMfaListener hook must be mounted whenever the user’s embedded wallet requires that they complete MFA.We recommend that you render this component near the root of your application, so that it is always rendered whenever the embedded wallet may be used.To set up this configuration, implement the
PrivyMfaPromptDelegate protocol and configure it via PrivyMFAConfig. When an operation requires MFA verification, the SDK will automatically call your delegate’s onMfaRequired method.Setting up the delegate
First, create a class that implements thePrivyMfaPromptDelegate protocol:MfaPromptHandler.swift
Report incorrect code
Copy
Ask AI
import PrivySDK
@MainActor
final class MfaPromptHandler: ObservableObject, PrivyMfaPromptDelegate {
@Published var isShowingPrompt = false
private var user: PrivyUser!
func onMfaRequired(for user: PrivyUser) {
self.user = user
self.isShowingPrompt = true
}
func submit(privy: Privy) {
// Code would be read from the view model
let code = "123456"
Task {
do {
try await user.mfa.totp.verify.submit(code: code)
await privy.mfa.resumeBlockedActions()
} catch {
// Show the error in UI so the user can retry
}
}
}
// Called if the user decides to cancel the flow
func cancel(privy: Privy) {
Task {
await privy.mfa.resumeBlockedActions(throwing: MfaError.cancelled)
}
}
}
Configuring the MFA delegate
Configure the delegate either at initialization or at runtime:Configure at initialization
Report incorrect code
Copy
Ask AI
let mfaHandler = MfaPromptHandler()
let mfaConfig = PrivyMFAConfig(delegate: mfaHandler)
let config = PrivyConfig(
appId: "your-app-id",
appClientId: "your-client-id",
mfaConfig: mfaConfig
)
let privy = PrivySdk.initialize(config: config)
Configure at runtime
Report incorrect code
Copy
Ask AI
// You can also set the config at runtime
let mfaHandler = MfaPromptHandler()
let mfaConfig = PrivyMFAConfig(delegate: mfaHandler)
privy.mfa.setConfig(mfaConfig)
Completing verification and resuming operations
After the user completes MFA verification, you must callresumeBlockedActions() to unblock any pending wallet operations:Report incorrect code
Copy
Ask AI
// After successful MFA verification
try await user.mfa.totp.verify.submit(code: mfaCode)
await privy.mfa.resumeBlockedActions()
If
resumeBlockedActions() is not called within 5 minutes of the operation that triggered the delegate, the original wallet operation will throw with a .embeddedWalletFailure(reason: .timeoutOnMfa) error.Handling cancellation
If the user cancels the MFA flow, you must still callresumeBlockedActions to unblock operations. Pass an error to indicate cancellation:Report incorrect code
Copy
Ask AI
// If the user cancels MFA
await privy.mfa.resumeBlockedActions(throwing: MyError.mfaCancelled)
Example MFA modal
See a recommended abstraction for guiding users to complete MFA
See a recommended abstraction for guiding users to complete MFA
To simplify the implementation, we recommend abstracting the logic into a self-contained component that can be used whenever the user needs to complete an MFA flow.For instance, you might write an Notice how the modal contains all logic for requesting the MFA code, submitting the MFA code, handling errors, and cancelling an in-progress MFA code.Then, when your app needs to prompt a user to complete MFA, they can simply display this
MFAModal component that allows the user to (1) select their desired method of their enrolled MFA methods, (2) request an MFA code, and (3) submit the MFA code to Privy for verification.- React
- React Native
- Swift
Example modal for guiding users through the MFA flow
Report incorrect code
Copy
Ask AI
import Modal from 'react-modal';
import {
useMfa,
errorIndicatesMfaVerificationFailed,
errorIndicatesMfaMaxAttempts,
errorIndicatesMfaTimeout,
MfaMethod,
} from '@privy-io/react-auth';
type Props = {
// List of available MFA methods that the user has enrolled in
mfaMethods: MfaMethod[];
// Boolean indicator to determine whether or not the modal should be open
isOpen: boolean;
// Helper function to open/close the modal */
setIsOpen: (isOpen: boolean) => void;
};
export const MFAModal = ({mfaMethods, isOpen, setIsOpen}: Props) => {
const {init, submit, cancel} = useMfa();
// Stores the user's selected MFA method
const [selectedMethod, setSelectedMethod] = useState<MfaMethod | null>(null);
// Stores the user's MFA code
const [mfaCode, setMfaCode] = useState('');
// Stores the options for passkey MFA
const [options, setOptions] = useState(null);
// Stores an error message to display
const [error, setError] = useState('');
// Helper function to request an MFA code for a given method
const onMfaInit = async (method: MfaMethod) => {
const response = await init(method);
setError('');
setSelectedMethod(method);
if (method === 'passkey') {
setOptions(response);
}
};
// Helper function to submit an MFA code to Privy for verification
const onMfaSubmit = async () => {
try {
if (selectedMethod === 'passkey') {
await submit(selectedMethod, options);
} else {
await submit(selectedMethod, mfaCode);
}
setSelectedMethod(null); // Clear the MFA flow once complete
setIsOpen(false); // Close the modal
} catch (e) {
// Handling possible errors with MFA code submission
if (errorIndicatesMfaVerificationFailed(e)) {
setError('Incorrect MFA code, please try again.');
// Allow the user to re-enter the code and call `submit` again
} else if (errorIndicatesMfaMaxAttempts(e)) {
setError('Maximum MFA attempts reached, please request a new code.');
setSelectedMethod(null); // Clear the MFA flow to allow the user to try again
} else if (errorIndicatesMfaTimeout(e)) {
setError('MFA code has expired, please request a new code.');
setSelectedMethod(null); // Clear the MFA flow to allow the user to try again
}
}
};
// Helper function to clean up state when the user closes the modal
const onModalClose = () => {
cancel(); // Cancel any in-progress MFA flows
setIsOpen(false);
};
return (
<Modal isOpen={isOpen} onAfterClose={onModalClose}>
{/* Button for the user to select an MFA method and request an MFA code */}
{mfaMethods.map((method) => (
<button onClick={() => onMfaInit(method)}>Choose to MFA with {method}</button>
))}
{/* Input field for the user to enter their MFA code and submit it */}
{selectedMethod && selectedMethod !== 'passkey' && (
<div>
<p>Enter your MFA code below</p>
<input placeholder="123456" onChange={(event) => setMfaCode(event.target.value)} />
<button onClick={() => onMfaSubmit()}>Submit Code</button>
</div>
)}
{/* Display error message if there is one */}
{!!error.length && <p>{error}</p>}
</Modal>
);
};
Example modal for guiding users through the MFA flow
Report incorrect code
Copy
Ask AI
import Modal from 'react-native';
import {
useMfa,
errorIndicatesMfaVerificationFailed,
errorIndicatesMfaMaxAttempts,
errorIndicatesMfaTimeout,
MfaMethod,
} from '@privy-io/expo';
type Props = {
// List of available MFA methods that the user has enrolled in
mfaMethods: MfaMethod[];
// Boolean indicator to determine whether or not the modal should be open
isOpen: boolean;
// Helper function to open/close the modal */
setIsOpen: (isOpen: boolean) => void;
};
export const MFAModal = ({mfaMethods, isOpen, setIsOpen}: Props) => {
const {init, submit, cancel} = useMfa();
const [selectedMethod, setSelectedMethod] = useState<MfaMethod | null>(null);
const [mfaCode, setMfaCode] = useState('');
const [options, setOptions] = useState(null);
const [error, setError] = useState('');
const onMfaInit = async (method: MfaMethod) => {
const response = await init({method});
setError('');
setSelectedMethod(method);
if (method === 'passkey') {
setOptions(response);
}
};
const onMfaSubmit = async () => {
try {
if (selectedMethod === 'passkey') {
await submit({method: selectedMethod, mfaCode: options});
} else {
await submit({method: selectedMethod, mfaCode});
}
setSelectedMethod(null);
setIsOpen(false);
} catch (e) {
if (errorIndicatesMfaVerificationFailed(e)) {
setError('Incorrect MFA code, please try again.');
} else if (errorIndicatesMfaMaxAttempts(e)) {
setError('Maximum MFA attempts reached, please request a new code.');
setSelectedMethod(null);
} else if (errorIndicatesMfaTimeout(e)) {
setError('MFA code has expired, please request a new code.');
setSelectedMethod(null);
}
}
};
const onModalClose = () => {
cancel();
setIsOpen(false);
};
return (
<Modal visible={isOpen} animationType="slide" onRequestClose={onModalClose}>
<View style={{padding: 20}}>
{mfaMethods.map((method) => (
<Button
key={method}
title={`Choose to MFA with ${method}`}
onPress={() => onMfaInit(method)}
/>
))}
{selectedMethod && selectedMethod !== 'passkey' && (
<View>
<Text>Enter your MFA code below</Text>
<TextInput
placeholder="123456"
value={mfaCode}
onChangeText={setMfaCode}
keyboardType="numeric"
style={{borderBottomWidth: 1, marginBottom: 10}}
/>
<Button title="Submit Code" onPress={onMfaSubmit} />
</View>
)}
{error.length > 0 && <Text style={{color: 'red'}}>{error}</Text>}
<Button title="Close" onPress={onModalClose} />
</View>
</Modal>
);
};
Example view for guiding users through the MFA flow
Report incorrect code
Copy
Ask AI
import PrivySDK
import SwiftUI
struct MFAView: View {
@Binding var isPresented: Bool
@State private var totpCode: String = ""
@State private var errorMessage: String?
@State private var isVerifying = false
let privy: Privy
let user: PrivyUser
var body: some View {
VStack(spacing: 24) {
Text("MFA Required")
.font(.headline)
Text("Enter your 6-digit TOTP code")
.font(.subheadline)
.foregroundColor(.secondary)
TextField("123456", text: $totpCode)
.keyboardType(.numberPad)
.multilineTextAlignment(.center)
.font(.system(size: 32, weight: .bold, design: .monospaced))
.frame(width: 180)
.disabled(isVerifying)
if let errorMessage {
Text(errorMessage)
.font(.caption)
.foregroundColor(.red)
.multilineTextAlignment(.center)
}
HStack(spacing: 16) {
Button("Cancel") {
cancel()
}
.foregroundColor(.red)
.disabled(isVerifying)
Button {
submit()
} label: {
if isVerifying {
ProgressView()
} else {
Text("Verify")
}
}
.buttonStyle(.borderedProminent)
.disabled(totpCode.count != 6 || isVerifying)
}
}
.padding(32)
}
private func submit() {
isVerifying = true
errorMessage = nil
let code = totpCode
Task {
do {
try await user.mfa.totp.verify.submit(code: code)
await privy.mfa.resumeBlockedActions()
isPresented = false
} catch {
errorMessage = error.localizedDescription
}
isVerifying = false
}
}
private func cancel() {
Task {
await privy.mfa.resumeBlockedActions(throwing: TestAppError.mfaCancelled)
}
isPresented = false
}
}
MFAModal component (or MFAView in Swift) and configure it with the list of MFA methods that are available for the current user.Catching MFA errors inline
- React / React Native
- Swift
The React and React Native SDKs do not support inline MFA errors, and require the listener to be set up for MFA flows.
As an alternative to registering a listener, your app can catch MFA errors directly when performing wallet operations. This approach gives you more control over when and how to prompt for MFA verification.When an operation requires MFA, the SDK throws a
.embeddedWalletFailure(reason: .mfaRequired) error. Your app can catch this error, prompt the user to complete MFA, and retry the operation.Report incorrect code
Copy
Ask AI
func signMessageWithMfa() async {
guard let user = privy.user else { return }
guard let wallet = user.embeddedSolanaWallets.first else { return }
let message = "SGVsbG8hIEkgYW0gdGhlIGJhc2U2NCBlbmNvZGVkIG1lc3NhZ2UgdG8gYmUgc2lnbmVkLg=="
do {
// Attempt the wallet operation
let signature = try await wallet.provider.signMessage(message: message)
print("Signature: \(signature)")
} catch PrivyError.embeddedWalletFailure(reason: .mfaRequired(let user)) {
// MFA is required - prompt user to verify
do {
// Show your MFA UI and collect the TOTP code
let totpCode = await showMfaPromptAndGetCode()
// Verify the MFA code
try await user.mfa.totp.verify.submit(code: totpCode)
// Retry the original operation after successful verification
let signature = try await wallet.provider.signMessage(message: message)
print("Signature: \(signature)")
} catch {
print("MFA verification failed: \(error.localizedDescription)")
}
} catch {
print("Wallet operation failed: \(error.localizedDescription)")
}
}
This inline approach works well for apps with simple MFA flows or when your app needs fine-grained control over the MFA experience. For more complex apps with multiple wallet operations, consider using the listener approach.

