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

# Post-login user setup

> Get a freshly signed-in user ready for your app — collect required fields, enroll MFA, register their device, and create an embedded wallet.

## Prerequisites

Before this page: create and initialize a Dynamic client (see [Creating a Dynamic client](https://docs.dynamic.xyz/docs/javascript/reference/client/create-dynamic-client.md) and [Initializing the Dynamic client](https://docs.dynamic.xyz/docs/javascript/reference/client/initialize-dynamic-client.md)), and get the user authenticated with [email/SMS OTP](https://docs.dynamic.xyz/docs/javascript/building-ui/email-sms-otp-sign-in.md), [social](https://docs.dynamic.xyz/docs/javascript/building-ui/social-oauth-redirect-handling.md), or [passkeys](https://docs.dynamic.xyz/docs/javascript/building-ui/passkey-sign-in-registration.md).

## What you'll build

Authentication proves *who* the user is. It doesn't guarantee they're ready to use your app. Depending on your [console configuration](https://console.dynamic.xyz/dashboard), a just-signed-in user may still need to:

1. **Complete required fields** — profile information you've marked required (name, email, custom fields).
2. **Enroll in MFA** — set up a TOTP authenticator app and save recovery codes. See [MFA](https://docs.dynamic.xyz/docs/javascript/authentication-methods/mfa/overview.md).
3. **Register their device** — confirm the device via an emailed link. See [Device registration](https://docs.dynamic.xyz/docs/javascript/authentication-methods/device-registration.md).
4. **Get an embedded wallet** — create their [embedded wallet](https://docs.dynamic.xyz/docs/embedded-wallets/mpc/overview.md) if your app uses them.

You'll build a small **setup flow** that runs these checks after login, shows UI for whatever is still pending, and lets the user into your app once everything is done.

> [!NOTE]
>
> Not every app needs every step. If you haven't enabled MFA or device registration in the Dynamic Console, those checks simply return `false` — build only the steps your app requires.

## The setup flow

After login, run a chain of checks. Each check answers "does the user still need to do this?" — if yes, show that step's UI, wait for the user to finish, then move to the next check.

```
signed in → required fields? → MFA? → device registration? → embedded wallet? → ready
```

## Detecting what's pending

Each check is a single call. Compose them into one object so the rest of your UI can ask "what's left?":

```typescript theme={"system"}
import { getDefaultClient } from '@dynamic-labs-sdk/client';
import {
  isUserMissingMfaAuth,
  isPendingRecoveryCodesAcknowledgment,
  isDeviceRegistrationRequired,
} from '@dynamic-labs-sdk/client';
import {
  isDynamicWaasEnabled,
  getChainsMissingWaasWalletAccounts,
} from '@dynamic-labs-sdk/client/waas';

function detectPendingSteps() {
  const user = getDefaultClient().user;
  if (!user) return null;

  return {
    missingFields: user.missingFields ?? [],
    needsFields: Boolean(user.missingFields?.length),
    needsMfa: isUserMissingMfaAuth(),
    needsRecoveryAck: isPendingRecoveryCodesAcknowledgment(),
    needsDeviceRegistration: isDeviceRegistrationRequired(user),
    needsWallet:
      isDynamicWaasEnabled() &&
      getChainsMissingWaasWalletAccounts().length > 0,
  };
}

function isSetupComplete(): boolean {
  const steps = detectPendingSteps();
  if (!steps) return false;

  return (
    !steps.needsFields &&
    !steps.needsMfa &&
    !steps.needsRecoveryAck &&
    !steps.needsDeviceRegistration &&
    !steps.needsWallet
  );
}
```

> [!TIP]
>
> Compose the checks yourself rather than gating on a single "onboarding complete" call. This keeps you in control of exactly which steps block access — for example, you might let users into the app before an embedded wallet exists and create it lazily later.

## Building the flow

> [tab] TypeScript
>
> Run each step in order. When a step needs the user, render your UI, wait for them to finish, then continue. The `show*` functions below are **your implementation** — the SDK owns the logic, you own the screens.
>
> ```typescript theme={"system"}
> import { updateUser } from '@dynamic-labs-sdk/client';
> import {
>   registerTotpMfaDevice,
>   authenticateTotpMfaDevice,
>   getMfaRecoveryCodes,
>   acknowledgeRecoveryCodes,
>   completeDeviceRegistration,
> } from '@dynamic-labs-sdk/client';
> import {
>   createWaasWalletAccounts,
>   getChainsMissingWaasWalletAccounts,
> } from '@dynamic-labs-sdk/client/waas';
>
> async function runSetup() {
>   const steps = detectPendingSteps();
>   if (!steps) return;
>
>   // 1. Required fields — collect values, then save them under `userFields`.
>   if (steps.needsFields) {
>     const values = await showFieldForm(steps.missingFields);
>     await updateUser({ userFields: values });
>   }
>
>   // 2. MFA enrollment — register a device, show the QR/secret, verify a code.
>   if (steps.needsMfa) {
>     const { uri, secret, id } = await registerTotpMfaDevice();
>     const code = await showTotpEnrollment({ uri, secret });
>     await authenticateTotpMfaDevice({ code, deviceId: id });
>   }
>
>   // 2b. Recovery codes — show them once and require acknowledgment.
>   if (steps.needsRecoveryAck) {
>     const { recoveryCodes } = await getMfaRecoveryCodes();
>     await showRecoveryCodes(recoveryCodes);
>     await acknowledgeRecoveryCodes();
>   }
>
>   // 3. Device registration — the user confirms via an emailed link (below).
>   if (steps.needsDeviceRegistration) {
>     showDeviceRegistrationPending();
>     const deviceToken = await waitForDeviceToken();
>     await completeDeviceRegistration({ deviceToken });
>   }
>
>   // 4. Embedded wallet — create it once the user is fully onboarded.
>   if (steps.needsWallet) {
>     showWalletProgress();
>     await createWaasWalletAccounts({
>       chains: getChainsMissingWaasWalletAccounts(),
>     });
>   }
>
>   showApp();
> }
> ```
>
> > [!NOTE]
> >
> > `updateUser` returns an `OTPVerification` when a **sensitive** field (email or phone) changes, because the new value must be verified with a code. For plain profile fields it resolves to `undefined`. If you collect email/phone here, follow up with the [OTP verification flow](https://docs.dynamic.xyz/docs/javascript/building-ui/email-sms-otp-sign-in.md).
>
> ### Device registration: completing the redirect
>
> Device registration emails the user a link. When they click it they return to your app with a token in the URL — detect and complete it on **every** page load, the same way you handle [social redirects](https://docs.dynamic.xyz/docs/javascript/building-ui/social-oauth-redirect-handling.md):
>
> ```typescript theme={"system"}
> import {
>   getDeviceRegistrationTokenFromUrl,
>   completeDeviceRegistration,
> } from '@dynamic-labs-sdk/client';
>
> async function completeDeviceRegistrationIfPresent(): Promise<boolean> {
>   const deviceToken = getDeviceRegistrationTokenFromUrl({
>     url: window.location.href,
>   });
>   if (!deviceToken) return false;
>
>   await completeDeviceRegistration({ deviceToken });
>
>   // Strip the token from the URL so a refresh doesn't replay it.
>   window.history.replaceState({}, '', window.location.pathname);
>   return true;
> }
> ```

> [tab] React
>
> Drive the flow with a wizard that renders the first pending step and advances as each completes. The mutation hooks give you `isPending`/`error` straight from TanStack Query.
>
> ```tsx theme={"system"}
> import { useState } from 'react';
> import { getDefaultClient } from '@dynamic-labs-sdk/client';
> import {
>   isUserMissingMfaAuth,
>   isDeviceRegistrationRequired,
> } from '@dynamic-labs-sdk/client';
> import {
>   isDynamicWaasEnabled,
>   getChainsMissingWaasWalletAccounts,
> } from '@dynamic-labs-sdk/client/waas';
> import { useUser } from '@dynamic-labs-sdk/react-hooks';
>
> type Step = 'fields' | 'mfa' | 'device' | 'wallet' | 'complete';
>
> const ORDER: Step[] = ['fields', 'mfa', 'device', 'wallet', 'complete'];
>
> function isStepNeeded(step: Step): boolean {
>   const user = getDefaultClient().user;
>   if (!user) return false;
>
>   switch (step) {
>     case 'fields':
>       return Boolean(user.missingFields?.length);
>     case 'mfa':
>       return isUserMissingMfaAuth();
>     case 'device':
>       return isDeviceRegistrationRequired(user);
>     case 'wallet':
>       return (
>         isDynamicWaasEnabled() &&
>         getChainsMissingWaasWalletAccounts().length > 0
>       );
>     default:
>       return false;
>   }
> }
>
> function nextStep(after: Step): Step {
>   for (let i = ORDER.indexOf(after) + 1; i < ORDER.length; i++) {
>     if (ORDER[i] === 'complete' || isStepNeeded(ORDER[i])) return ORDER[i];
>   }
>   return 'complete';
> }
>
> // Render children only once every pending step is done.
> function SetupGate({ children }: { children: React.ReactNode }) {
>   const { data: user } = useUser();
>   const [step, setStep] = useState<Step>(() =>
>     ORDER.find(isStepNeeded) ?? 'complete',
>   );
>
>   if (!user) return null;
>   if (step === 'complete') return <>{children}</>;
>
>   const advance = () => setStep(nextStep(step));
>
>   if (step === 'fields') return <FieldsStep onDone={advance} />;
>   if (step === 'mfa') return <MfaStep onDone={advance} />;
>   if (step === 'device') return <DeviceStep onDone={advance} />;
>   return <WalletStep onDone={advance} />;
> }
> ```
>
> ### Required fields step
>
> ```tsx theme={"system"}
> import type { UpdateUserFields } from '@dynamic-labs-sdk/client';
> import { useUser, useUpdateUser } from '@dynamic-labs-sdk/react-hooks';
>
> function FieldsStep({ onDone }: { onDone: () => void }) {
>   const { data: user } = useUser();
>   const { mutateAsync: updateUser, isPending } = useUpdateUser();
>
>   const handleSubmit = async (event: React.FormEvent<HTMLFormElement>) => {
>     event.preventDefault();
>     // The inputs are named after the fields your project requires, so the
>     // form values are the user fields to send.
>     const formValues = Object.fromEntries(
>       new FormData(event.currentTarget).entries()
>     );
>     await updateUser({ userFields: formValues as UpdateUserFields });
>     onDone();
>   };
>
>   return (
>     <form onSubmit={handleSubmit}>
>       <h2>Complete your profile</h2>
>       {user?.missingFields?.map((field) => (
>         <label key={field.name}>
>           {field.name}
>           <input name={field.name} required />
>         </label>
>       ))}
>       <button type="submit" disabled={isPending}>
>         {isPending ? 'Saving…' : 'Continue'}
>       </button>
>     </form>
>   );
> }
> ```
>
> ### Embedded wallet step
>
> `useCreateWaasWalletAccounts` is a mutation — trigger it when the step opens (or behind a button) and advance once it resolves.
>
> ```tsx theme={"system"}
> import { getChainsMissingWaasWalletAccounts } from '@dynamic-labs-sdk/client/waas';
> import { useCreateWaasWalletAccounts } from '@dynamic-labs-sdk/react-hooks';
>
> function WalletStep({ onDone }: { onDone: () => void }) {
>   const { mutateAsync: createWallet, isPending, error } =
>     useCreateWaasWalletAccounts();
>
>   return (
>     <div>
>       <h2>Setting up your wallet</h2>
>       <p>We're creating a secure embedded wallet for your account.</p>
>       <button
>         type="button"
>         disabled={isPending}
>         onClick={async () => {
>           await createWallet({ chains: getChainsMissingWaasWalletAccounts() });
>           onDone();
>         }}
>       >
>         {isPending ? 'Creating…' : 'Create wallet'}
>       </button>
>       {error ? <p role="alert">{error.message}</p> : null}
>     </div>
>   );
> }
> ```
>
> The MFA and device-registration steps follow the same shape with `useRegisterTotpMfaDevice` / `useAuthenticateTotpMfaDevice` and `useCompleteDeviceRegistration`.

## Handling errors

Each step can fail independently. Keep the user on the current step when its call rejects, rather than dropping them back to the start.

| Step | What can go wrong | What to do |
| - | - | - |
| Required fields | Validation error, or a sensitive field needs OTP verification | Show the field-level message; if `updateUser` returns an `OTPVerification`, run the code-entry flow. |
| MFA enrollment | Wrong TOTP code | Keep the enrollment screen open and let the user re-enter the code. |
| Recovery codes | Acknowledgment fails | Re-show the codes and retry `acknowledgeRecoveryCodes`. |
| Device registration | Expired or invalid link token | Ask the user to request a new email and try again. |
| Embedded wallet | Creation fails | Surface a retry; the user is signed in, so you can retry without re-authenticating. |

## Alternative approaches

### Gate access without per-step UI

If you don't need a wizard, just block the app until the user is ready by reusing `isSetupComplete()`:

> [tab] TypeScript
>
> ```typescript theme={"system"}
> if (!isSetupComplete()) {
>   redirectToSetup();
> }
> ```

> [tab] React
>
> ```tsx theme={"system"}
> import { useUser } from '@dynamic-labs-sdk/react-hooks';
>
> function OnboardingGate({ children }: { children: React.ReactNode }) {
>   const { data: user } = useUser();
>
>   if (!user) return null;
>   if (!isSetupComplete()) return <SetupPage />;
>
>   return <>{children}</>;
> }
> ```

### React to completion from another tab

Listen for `userChanged` to advance when the user finishes a step elsewhere (e.g. clicking the device-registration email in another tab):

```typescript theme={"system"}
import { onEvent } from '@dynamic-labs-sdk/client';

onEvent({
  event: 'userChanged',
  listener: () => {
    if (isSetupComplete()) showApp();
  },
});
```

### Skip steps with your own rules

The chain is yours — branch it on your business logic. For example, only require MFA for users who will access sensitive features:

```typescript theme={"system"}
import { updateUser } from '@dynamic-labs-sdk/client';
async function runSetup({ requireMfa }: { requireMfa: boolean }) {
  const steps = detectPendingSteps();
  if (!steps) return;

  if (steps.needsFields) {
    const values = await showFieldForm(steps.missingFields);
    await updateUser({ userFields: values });
  }

  if (steps.needsMfa && requireMfa) {
    // ...run the MFA enrollment step
  }
}
```

## See also

* [Email & SMS OTP sign-in](https://docs.dynamic.xyz/docs/javascript/building-ui/email-sms-otp-sign-in.md) — get the user authenticated first
* [MFA](https://docs.dynamic.xyz/docs/javascript/authentication-methods/mfa/overview.md) — MFA enrollment and verification reference
* [Device registration](https://docs.dynamic.xyz/docs/javascript/authentication-methods/device-registration.md) — the device registration flow
* [Creating embedded wallets](https://docs.dynamic.xyz/docs/javascript/reference/waas/creating-waas-wallet-accounts.md) — embedded wallet creation reference
* [Authentication concepts](https://docs.dynamic.xyz/docs/auth/concepts.md) — users, sessions, and credentials


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.