> ## 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](/docs/javascript/reference/client/create-dynamic-client) and [Initializing the Dynamic client](/docs/javascript/reference/client/initialize-dynamic-client)), and get the user authenticated with [email/SMS OTP](/docs/javascript/building-ui/email-sms-otp-sign-in), [social](/docs/javascript/building-ui/social-oauth-redirect-handling), or [passkeys](/docs/javascript/building-ui/passkey-sign-in-registration).

## What you'll build

Authentication proves *who* the user is. It doesn't guarantee they're ready to use your app. Depending on your [dashboard configuration](https://app.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](/docs/javascript/authentication-methods/mfa/overview).
3. **Register their device** — confirm the device via an emailed link. See [Device registration](/docs/javascript/authentication-methods/device-registration).
4. **Get an embedded wallet** — create their [embedded wallet](/docs/overview/wallets/embedded-wallets/mpc/overview) 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 dashboard, those checks simply return `false` — build only the steps your app requires.
</Note>

## 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.
</Tip>

## Building the flow

<Tabs>
  <Tab title="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 } 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();
      }

      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](/docs/javascript/building-ui/email-sms-otp-sign-in).
    </Note>

    ### 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](/docs/javascript/building-ui/social-oauth-redirect-handling):

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

    async function completeDeviceRegistrationIfPresent(): Promise<boolean> {
      const deviceToken = getDeviceRegistrationTokenFromUrl();
      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>

  <Tab title="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 { 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();
        const userFields = Object.fromEntries(new FormData(event.currentTarget));
        await updateUser({ userFields });
        onDone();
      };

      return (
        <form onSubmit={handleSubmit}>
          <h2>Complete your profile</h2>
          {user?.missingFields?.map((field) => (
            <label key={field}>
              {field}
              <input name={field} 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 { 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();
              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`.
  </Tab>
</Tabs>

## 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()`:

<Tabs>
  <Tab title="TypeScript">
    ```typescript theme={"system"}
    if (!isSetupComplete()) {
      redirectToSetup();
    }
    ```
  </Tab>

  <Tab title="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}</>;
    }
    ```
  </Tab>
</Tabs>

### 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"}
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](/docs/javascript/building-ui/email-sms-otp-sign-in) — get the user authenticated first
* [MFA](/docs/javascript/authentication-methods/mfa/overview) — MFA enrollment and verification reference
* [Device registration](/docs/javascript/authentication-methods/device-registration) — the device registration flow
* [Creating embedded wallets](/docs/javascript/reference/waas/creating-waas-wallet-accounts) — embedded wallet creation reference
* [Authentication concepts](/docs/overview/authentication/concepts) — users, sessions, and credentials
