> ## Documentation Index
> Fetch the complete documentation index at: https://docs.privy.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Advanced topics

> Advanced MFA topics including registering multiple MFA methods and managing MFA enrollment

## Multiple MFA Methods

**Privy allows users to register multiple MFA methods (e.g. SMS, TOTP, and passkeys) if they wish.**

<Info>
  If a user already has one MFA method enabled, and wishes to enroll in another (or change their
  existing MFA method), Privy will require the user to complete MFA with their existing method in
  order to complete enrollment of the new, additional method.
</Info>

When a user has multiple MFA methods enrolled, they will be given a choice of which method to use during verification:

### Accessing enrolled methods

You can check which MFA methods are available for the current user through the `user` object:

```tsx theme={"system"}
const {user} = usePrivy();
console.log(user.mfaMethods);
// ['sms', 'totp', 'passkey'] for a user who has enrolled in all three methods
```

This can be useful for determining which options to present to users in your custom UIs.

## Unenrolling MFA Methods

### Default UI approach

When using Privy's default UIs, users can remove MFA methods directly through the MFA management modal. Simply call the same method used for enrollment:

<View title="React" icon="react">
  ```tsx const {showMfaEnrollmentModal} = useMfaEnrollment(); // The same modal allows users to add theme={"system"}
  or remove MFA methods
  <button onClick={showMfaEnrollmentModal}>Manage MFA Methods</button>
  ```
</View>

<View title="React Native" icon="react">
  ```tsx theme={"system"}
  const {init: initMfaEnrollmentUI} = useMfaEnrollmentUI();

  const onManageMfa = async () => {
    await initMfaEnrollmentUI({mfaMethods: ['sms', 'totp', 'passkey']});
  };

  <Button onPress={onManageMfa}>Manage MFA Methods</Button>
  ```
</View>

### Custom UI approach

For custom UIs, Privy provides methods to unenroll specific MFA methods:

<View title="React" icon="react">
  ```tsx theme={"system"}
  import {useMfaEnrollment} from '@privy-io/react-auth';

  export default function MfaUnenrollment() {
    const {unenrollWithSms, unenrollWithTotp, unenrollWithPasskey} = useMfaEnrollment();

    return (
      <div>
        <button onClick={unenrollWithSms}>Unenroll SMS</button>
        <button onClick={unenrollWithTotp}>Unenroll TOTP</button>
        <button onClick={unenrollWithPasskey}>Unenroll passkey</button>
      </div>
    );
  }
  ```
</View>

<View title="React Native" icon="react">
  ```tsx theme={"system"}
  import {useMfaEnrollment} from '@privy-io/expo';

  export default function MfaUnenrollment() {
    const {unenrollMfa} = useMfaEnrollment();

    return (
      <YStack>
        <Button onClick={() => unenrollMfa({method: 'sms'})}>
          <Text>Unenroll SMS</Text>
        </Button>
        <Button onClick={() => unenrollMfa({method: 'totp'})}>
          <Text>Unenroll TOTP</Text>
        </Button>
        <Button onClick={() => unenrollMfa({method: 'passkey'})}>
          <Text>Unenroll passkey</Text>
        </Button>
      </YStack>
    );
  }
  ```
</View>

When invoked, unenrollment methods will require the user to first complete MFA verification to confirm unenrollment. This ensures that only the authorized user can remove security methods.

### Special considerations for passkeys

By default, unenrolling a passkey as an MFA method will also unlink it as a valid login method. You can modify this behavior to keep the passkey as a login method:

<View title="React" icon="react">
  ```tsx // Configure in the PrivyProvider theme={"system"}
  <PrivyProvider
    appId="your-privy-app-id"
    config={{
      ...theRestOfYourConfig,
      passkeys: {
        // Set this to `false` to keep the passkey as a login method after unenrolling from MFA
        shouldUnlinkOnUnenrollMfa: false
      }
    }}
  >
    {/* your app's content */}
  </PrivyProvider>
  ```
</View>

<View title="React Native" icon="react">
  ```tsx // Option 1: Configure in the PrivyElements component theme={"system"}
  <PrivyElements config={{passkeys: {shouldUnlinkOnUnenrollMfa: false}}} />
  // Option 2: Use the removeForLogin parameter directly in the unenrollMfa call
  <Button onClick={() => unenrollMfa({method: 'passkey', removeForLogin: false})}>
    <Text>Unenroll passkey, keep as login method</Text>
  </Button>
  ```
</View>

## Error Handling for MFA Codes

When users are submitting MFA codes (both during enrollment and verification), several types of errors can occur:

1. **Incorrect code**: The user entered the wrong MFA code
2. **Maximum attempts reached**: The user has tried too many incorrect codes
3. **Timeout**: The MFA code has expired and a new one must be requested

### Helper functions for error types

Privy provides helper functions to identify the type of error:

<View title="React" icon="react">
  ```tsx theme={"system"}
  import {
    errorIndicatesMfaVerificationFailed,
    errorIndicatesMaxMfaRetries,
    errorIndicatesMfaTimeout
  } from '@privy-io/react-auth';
  try {
    await submit('sms', userEnteredCode);
  } catch (e) {
    if (errorIndicatesMfaVerificationFailed(e)) {
      // Wrong code - allow the user to try again
      setError('Incorrect code. Please try again.');
    } else if (errorIndicatesMaxMfaRetries(e)) {
      // Too many attempts - request a new code
      setError('Too many failed attempts. Please request a new code.');
      resetMfaFlow();
    } else if (errorIndicatesMfaTimeout(e)) {
      // Code expired - request a new code
      setError('Your code has expired. Please request a new code.');
      resetMfaFlow();
    } else {
      // Some other error occurred
      setError('An unexpected error occurred. Please try again later.');
      console.error(e);
    }
  }
  ```
</View>

<View title="React Native" icon="react">
  ```tsx theme={"system"}
  import {
    errorIndicatesMfaVerificationFailed,
    errorIndicatesMaxMfaRetries,
    errorIndicatesMfaTimeout
  } from '@privy-io/expo';
  try {
    await submit({method: 'sms', mfaCode: userEnteredCode});
  } catch (e) {
    if (errorIndicatesMfaVerificationFailed(e)) {
      // Wrong code - allow the user to try again
      setError('Incorrect code. Please try again.');
    } else if (errorIndicatesMaxMfaRetries(e)) {
      // Too many attempts - request a new code
      setError('Too many failed attempts. Please request a new code.');
      resetMfaFlow();
    } else if (errorIndicatesMfaTimeout(e)) {
      // Code expired - request a new code
      setError('Your code has expired. Please request a new code.');
      resetMfaFlow();
    } else {
      // Some other error occurred
      setError('An unexpected error occurred. Please try again later.');
      console.error(e);
    }
  }
  ```
</View>

### Best practices for handling errors

1. **Clear error messaging**: Provide specific feedback based on the error type
2. **Reset flow after max attempts**: After the maximum number of attempts, clear state and allow the user to request a new code
3. **Timeouts**: Inform users when codes expire and provide a way to request new ones
4. **Exponential backoff**: Consider implementing a slight delay after multiple failures to discourage brute force attempts

## Optimistic MFA Verification

For an optimal user experience, there are cases where you may want to prompt for MFA verification *before* it's automatically required by the system. This is called "optimistic verification."

<Info>
  An example use case: A user with TOTP enabled wants to add SMS as a second MFA method. Instead of
  waiting for the system to prompt for TOTP verification during the SMS enrollment, you can
  proactively ask for TOTP verification upfront for a smoother user flow.
</Info>

### Using promptMfa for optimistic verification

<View title="React" icon="react">
  ```tsx theme={"system"}
  const {promptMfa} = useMfa();
  const {initEnrollmentWithSms} = useMfaEnrollment();

  const handleAddSmsMethod = async () => {
    // Proactively verify with existing MFA method first
    await promptMfa();

    // After successful verification, proceed with new method enrollment
    const phoneNumber = await getUserPhoneInput();
    await initEnrollmentWithSms({phoneNumber});

    // Continue with SMS enrollment flow...
  };
  ```
</View>

<View title="React Native" icon="react">
  ```tsx theme={"system"}
  const {promptMfa} = useMfa();
  const {initMfaEnrollment} = useMfaEnrollment();

  const handleAddSmsMethod = async () => {
    // Proactively verify with existing MFA method first
    await promptMfa();

    // After successful verification, proceed with new method enrollment
    const phoneNumber = await getUserPhoneInput();
    await initMfaEnrollment({method: 'sms', phoneNumber});

    // Continue with SMS enrollment flow...
  };
  ```
</View>

The `promptMfa` method will trigger the MFA verification flow only if necessary (if the user doesn't already have an active MFA token). If verification is successful, a token is cached for 15 minutes, and subsequent operations won't require additional MFA verification.

### Benefits of optimistic verification

1. **Improved UX**: Consolidate authentication steps at logical points in the user journey
2. **Reduced friction**: Avoid interrupting users in the middle of a multi-step process
3. **Predictable flow**: Create a more controlled and predictable verification experience

Keep in mind that optimistic verification is entirely optional - the wallet will always require MFA when needed. This approach simply gives you more control over when and how the verification occurs.

## Invalidating the cached MFA token

After a successful `promptMfa` call, Privy caches the MFA token for 15 minutes. To manually invalidate it before expiry — for example, when the user backgrounds the app or logs out — use the `clear` method from `useMfa`:

<View title="React" icon="react">
  ```tsx theme={"system"}
  const {clear} = useMfa();

  const handleLogout = async () => {
  clear();
  await logout();
  };

  ```
</View>

<View title="React Native" icon="react">
  ```tsx theme={"system"}
  const {clear} = useMfa();

  const handleLogout = async () => {
    clear();
    await logout();
  };
  ```
</View>

<Info>
  For a full example of tying MFA token invalidation to app lifecycle events on React Native, see
  the [MFA on app session](/recipes/react-native/mfa-on-app-session) recipe.
</Info>
