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

# Upgrading legacy embedded wallets to Dynamic WaaS

> Detect users whose embedded wallets need upgrading and move them onto Dynamic WaaS with the ready-made upgrade app (by redirect, embed, or popup) using the legacy-embedded-wallet-upgrade package.

## Prerequisites

Before this page: users who sign in to your app (see [Post-login user setup](https://docs.dynamic.xyz/docs/javascript/building-ui/post-auth-user-setup.md)) and a Dynamic environment with WaaS enabled.

## What you'll build

Some users may still hold an earlier, legacy generation of embedded wallet. The JavaScript SDK does **not** support these wallets: no extension registers a provider for them, so a user on a legacy wallet cannot sign transactions or messages, and calls fail with a `NoWalletProviderFoundError`. Upgrading to Dynamic WaaS is required for those users to use their wallets.

Dynamic provides a ready-made **upgrade app** that performs the upgrade, and the wallet keeps the **same address**. Because the upgrade happens in a dedicated app (hosted by Dynamic or on your own domain, see [Hosting the app](#hosting-the-app)), the only supported path is routing the user to it; there is no in-SDK upgrade.

The `@dynamic-labs-sdk/legacy-embedded-wallet-upgrade` package wraps the whole flow so you only call functions: it derives the environment from your client, builds the app URL, validates the return URL, and handles the completion handshake. Your job is to:

1. **Detect** which users need the upgrade.
2. **Route** them to the upgrade app (by redirect, by embedding it, or by popup).
3. **React** when the upgrade finishes.

This page covers what's shared across the routes. Each route then has its own page:

* [Redirect to the app](https://docs.dynamic.xyz/docs/javascript/building-ui/legacy-wallet-upgrade/redirect.md): navigate away and read the outcome on return.
* [Embed the app (iframe)](https://docs.dynamic.xyz/docs/javascript/building-ui/legacy-wallet-upgrade/embed.md): render it inline in a sheet or modal.
* [Popup (new window)](https://docs.dynamic.xyz/docs/javascript/building-ui/legacy-wallet-upgrade/popup.md): open it in a separate window. **The recommended route for React Native**, where it runs in the system auth session.

If none of those fit, [going lower-level](https://docs.dynamic.xyz/docs/javascript/building-ui/legacy-wallet-upgrade/lower-level.md) lets you build the URL and own the mounting yourself.

## Install

```bash theme={"system"}
npm install @dynamic-labs-sdk/legacy-embedded-wallet-upgrade
```

> [!NOTE]
>
> The React entry point (`/react`) has `react` and `@dynamic-labs-sdk/react-hooks` as **optional** peer dependencies. Non-React apps import the root and never pull React in.

## Detect who needs the upgrade

Two synchronous helpers tell you whether the signed-in user has any wallet that needs upgrading. They read the environment and wallets from your client, so single-client apps call them with no arguments.

> [tab] TypeScript
>
> ```typescript theme={"system"}
> import {
>   hasWalletsRequiringUpgrade,
>   getWalletsRequiringUpgrade,
> } from '@dynamic-labs-sdk/legacy-embedded-wallet-upgrade';
>
> function maybePromptUpgrade() {
>   if (!hasWalletsRequiringUpgrade()) return;
>
>   const wallets = getWalletsRequiringUpgrade();
>   // showUpgradeBanner is your implementation.
>   showUpgradeBanner(`${wallets.length} wallet(s) can be upgraded.`);
> }
> ```

> [tab] React / React Native
>
> ```tsx theme={"system"}
> import {
>   useHasWalletsRequiringUpgrade,
>   useWalletsRequiringUpgrade,
> } from '@dynamic-labs-sdk/legacy-embedded-wallet-upgrade/react';
>
> function UpgradeBanner() {
>   const needsUpgrade = useHasWalletsRequiringUpgrade();
>   const wallets = useWalletsRequiringUpgrade();
>
>   if (!needsUpgrade) return null;
>
>   return <p>{wallets.length} wallet(s) can be upgraded.</p>;
> }
> ```

## Choose a route

| Route | When to use |
| - | - |
| [Redirect](https://docs.dynamic.xyz/docs/javascript/building-ui/legacy-wallet-upgrade/redirect.md) | Simplest: you don't mind a full-page navigation away and back. |
| [Embed (iframe)](https://docs.dynamic.xyz/docs/javascript/building-ui/legacy-wallet-upgrade/embed.md) | You want the app inline in your own sheet/modal and never leave the page. Web only. |
| [Popup](https://docs.dynamic.xyz/docs/javascript/building-ui/legacy-wallet-upgrade/popup.md) | A redirect is too heavy but you don't want it inline. **Use this for React Native** since it runs in the system auth session. |

## Hosting the app

By default every helper points at Dynamic's hosted upgrade app. You can run it two ways:

1. **Dynamic-hosted**: use the shared Dynamic-hosted deployment as-is (the default; pass nothing).
2. **Your own subdomain**: point a CNAME (e.g. `upgrade.yourapp.com`) at the deployment so it runs on your own domain and branding.

For option 2, tell the package where the app lives with `baseUrl`. Every entry point accepts it:

```typescript theme={"system"}
import { startUpgradeRedirect, openUpgradeIframe, openUpgradePopup, buildUpgradeUrl, subscribeToUpgradeCompletion } from '@dynamic-labs-sdk/legacy-embedded-wallet-upgrade';
const baseUrl = 'https://upgrade.yourapp.com';

// Redirect
void startUpgradeRedirect({ baseUrl });

// Embed
const session = openUpgradeIframe({ baseUrl, container });

// Popup
const popupSession = openUpgradePopup({ baseUrl });

// Low-level
const uri = buildUpgradeUrl({ baseUrl });
const unsubscribe = subscribeToUpgradeCompletion({ baseUrl, onComplete });
```

## Passkey users require your own domain

Passkeys are bound to the domain they were registered on: the authenticator ties each passkey to the site's domain (its WebAuthn relying party ID) and the browser only offers it back on that domain or its subdomains. A passkey created on `yourapp.com` works on `upgrade.yourapp.com` but not on `upgrade-wallet.dynamic.dev`, so passkey users cannot use it in the Dynamic-hosted app, whether as their sign-in credential or as an MFA factor.

If any of your users use passkeys for sign-in or MFA, serve the upgrade app on the domain your passkeys were registered on (such as a subdomain like `upgrade.yourapp.com` when they were registered on `yourapp.com`) and set `baseUrl` accordingly. This is a hard requirement, not a UX preference.

## Required: allowlist the app's origin (CORS)

Add the origin serving the app to your environment's **allowed origins** in the Dynamic Console, or the app's calls will be blocked:

* **Your own subdomain** → add your origin (e.g. `https://upgrade.yourapp.com`).
* **Dynamic-hosted** → add `https://upgrade-wallet.dynamic.dev`.

If the app can't reach Dynamic, it shows an "add your domain to allowed origins" screen, a signal that the serving origin isn't allowlisted.

## Custom auth domains

If your environment authenticates through a **custom domain** (e.g. `auth.yourapp.com` in place of Dynamic's default API), the Dynamic-hosted upgrade app only authenticates against that domain once it's on Dynamic's internal allowlist. Environments that already had a custom domain configured when Dynamic v5 shipped are allowlisted, so the flow works out of the box.

> [!NOTE]
>
> Set up a custom domain more recently and the upgrade fails to reach your API? Reach out to Dynamic to have your domain added to the allowlist.

## Smoother UX: skip the extra login

If you serve the app on a **subdomain of your app's domain** and your environment uses **cookie-based sessions**, an already-signed-in user lands in the app already authenticated, with no second login. Otherwise the user simply signs in again with the same credential they use in your app.

> [!TIP]
>
> Tell the just-arrived user to sign in with the **same credential** they use in your app: that's how the app finds the wallet to upgrade.

## Handling errors

| Situation | What to do |
| - | - |
| Outcome `status` is `error` | The upgrade failed or was cancelled; offer a retry. |
| App calls blocked | The serving origin isn't allowlisted; add it to the environment's allowed origins. |

Route-specific errors (blocked/closed popups, etc.) are covered on each route's page.

## See also

* [Post-login user setup](https://docs.dynamic.xyz/docs/javascript/building-ui/post-auth-user-setup.md): detect upgrade needs alongside other post-login steps
* [Connected wallets management](https://docs.dynamic.xyz/docs/javascript/building-ui/connected-wallets-management.md): the upgraded wallet appears here with the same address
* [Wallet backup & recovery](https://docs.dynamic.xyz/docs/javascript/building-ui/wallet-backup-recovery.md): back up the wallet after upgrading


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