> ## Documentation Index
> Fetch the complete documentation index at: https://www.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](/docs/javascript/building-ui/post-auth-user-setup)) and a Dynamic environment with WaaS enabled.

## What you'll build

Some users may still hold an earlier, legacy generation of embedded wallet that should be moved onto Dynamic WaaS. Dynamic provides a ready-made **upgrade app** that performs the upgrade — the wallet keeps the **same address**.

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.

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

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

<Tabs>
  <Tab title="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>

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

<Warning>
  The upgrade app only accepts an **https** return URL. Serve your app over https (in local development, expose it with a tunnel such as ngrok) or the flow will be rejected.
</Warning>

## Option A — Redirect to the app

Navigate the user to the upgrade app, then read the outcome when they return. `startUpgradeRedirect` builds the URL and navigates; `consumeUpgradeRedirect` reads and strips the result parameter, refreshes the session on success, and returns the outcome (`null` when the user didn't just come back from an upgrade).

<Tabs>
  <Tab title="TypeScript">
    ```typescript theme={"system"}
    import {
      startUpgradeRedirect,
      consumeUpgradeRedirect,
    } from '@dynamic-labs-sdk/legacy-embedded-wallet-upgrade';

    // On your "Upgrade wallet" button. redirectUrl defaults to the current page.
    function startUpgrade() {
      void startUpgradeRedirect();
    }

    // Run once on the page the user returns to.
    async function handleUpgradeReturn() {
      const outcome = await consumeUpgradeRedirect();
      if (!outcome) return;

      if (outcome.status === 'success') {
        // The session was already refreshed for you. refreshUi is your implementation.
        refreshUi();
      } else {
        // offerRetry is your implementation — the upgrade failed or was cancelled.
        offerRetry();
      }
    }
    ```
  </Tab>

  <Tab title="React">
    ```tsx theme={"system"}
    import {
      startUpgradeRedirect,
      useUpgradeRedirectResult,
    } from '@dynamic-labs-sdk/legacy-embedded-wallet-upgrade/react';

    function UpgradeButton() {
      // Consumes the redirect result on mount, then reports the outcome.
      useUpgradeRedirectResult({
        // showToast is your implementation.
        onComplete: ({ status }) => showToast(status),
        onError: (error) => showToast('error'),
      });

      return (
        <button type="button" onClick={() => void startUpgradeRedirect()}>
          Upgrade wallet
        </button>
      );
    }
    ```
  </Tab>
</Tabs>

## Option B — Embed the app (iframe / WebView)

Render the app in an iframe or WebView instead of navigating away. When embedded, the app **posts an origin-checked completion message** instead of redirecting, so your page can react and close the embed. The package owns the message contract; you keep full control of the surrounding UI (e.g. your bottom sheet or modal).

<Tabs>
  <Tab title="TypeScript">
    `openUpgradeIframe` appends an iframe into a container you provide and resolves `completed` when the app reports done (refreshing the session on success). Call `close()` if the user dismisses your sheet first.

    ```typescript theme={"system"}
    import { openUpgradeIframe } from '@dynamic-labs-sdk/legacy-embedded-wallet-upgrade';

    async function embedUpgrade(container: HTMLElement) {
      const session = openUpgradeIframe({ container });

      const outcome = await session.completed;
      if (outcome.status === 'success') {
        // Session already refreshed. closeSheet is your implementation.
        closeSheet();
      } else {
        offerRetry();
      }
    }

    // If the user dismisses your sheet before completion:
    // session.close();
    ```
  </Tab>

  <Tab title="React">
    `useUpgradeIframe` renders no DOM — it returns the `upgradeUrl` for you to put in your own `<iframe>`, and attaches the origin-checked completion listener while `active`.

    ```tsx theme={"system"}
    import { useState } from 'react';
    import { useUpgradeIframe } from '@dynamic-labs-sdk/legacy-embedded-wallet-upgrade/react';

    function UpgradeSheet() {
      const [isOpen, setIsOpen] = useState(false);

      const { upgradeUrl } = useUpgradeIframe({
        active: isOpen,
        // Session already refreshed on success. showToast is your implementation.
        onComplete: ({ status }) => {
          showToast(status);
          setIsOpen(false);
        },
      });

      return (
        <>
          <button type="button" onClick={() => setIsOpen(true)}>
            Upgrade wallet
          </button>
          {isOpen && upgradeUrl ? (
            <iframe title="Wallet upgrade" src={upgradeUrl} />
          ) : null}
        </>
      );
    }
    ```
  </Tab>

  <Tab title="React Native">
    There is no DOM iframe in React Native, so use the two DOM-agnostic primitives with a `WebView`: `buildUpgradeUrl` for the `source` and `subscribeToUpgradeCompletion` to receive the result.

    ```tsx theme={"system"}
    import { useEffect } from 'react';
    import { WebView } from 'react-native-webview';
    import {
      buildUpgradeUrl,
      subscribeToUpgradeCompletion,
    } from '@dynamic-labs-sdk/legacy-embedded-wallet-upgrade';

    function UpgradeWebView() {
      useEffect(() => {
        // Session already refreshed on success. onDone is your implementation.
        const unsubscribe = subscribeToUpgradeCompletion({
          onComplete: ({ status }) => onDone(status),
        });
        return unsubscribe;
      }, []);

      return <WebView source={{ uri: buildUpgradeUrl() }} />;
    }
    ```
  </Tab>
</Tabs>

## Option C — Popup (new window)

Open the app in a popup window instead of navigating away or embedding it. Like the iframe path, the app **posts an origin-checked completion message** back — here to the window that opened it — so the current page never unloads. Reach for this when a full-page redirect is too heavy but you don't want the app inline.

<Warning>
  Browsers block popups that aren't opened from a user gesture. Call `openUpgradePopup` (or the hook's `open`) **synchronously inside the click handler** — don't `await` anything before it, or the popup is blocked and you get an `UpgradePopupBlockedError`.
</Warning>

<Tabs>
  <Tab title="TypeScript">
    `openUpgradePopup` opens the window synchronously and resolves `completed` when the app reports done (refreshing the session on success). `completed` rejects with an `UpgradePopupClosedError` if the user closes the popup first, and the call throws an `UpgradePopupBlockedError` if the browser blocks it.

    ```typescript theme={"system"}
    import {
      openUpgradePopup,
      UpgradePopupClosedError,
    } from '@dynamic-labs-sdk/legacy-embedded-wallet-upgrade';

    // On your "Upgrade wallet" button's click handler — no await before this call.
    function startUpgrade() {
      const session = openUpgradePopup();

      session.completed
        .then((outcome) => {
          if (outcome.status === 'success') {
            // Session already refreshed. refreshUi is your implementation.
            refreshUi();
          } else {
            offerRetry();
          }
        })
        .catch((error) => {
          if (error instanceof UpgradePopupClosedError) {
            // The user dismissed the popup before finishing.
            offerRetry();
          }
        });

      // If you need to dismiss it yourself (e.g. the user navigates away):
      // session.close();
    }
    ```
  </Tab>

  <Tab title="React">
    `useUpgradePopup` wraps the popup lifecycle: call `open` from a click handler and handle the result in `onComplete` / `onError`. The popup is closed for you on unmount.

    ```tsx theme={"system"}
    import { useUpgradePopup } from '@dynamic-labs-sdk/legacy-embedded-wallet-upgrade/react';

    function UpgradeButton() {
      const { open } = useUpgradePopup({
        // Session already refreshed on success. showToast is your implementation.
        onComplete: ({ status }) => showToast(status),
        // Fires on a dismissed popup (UpgradePopupClosedError), a blocked popup
        // (UpgradePopupBlockedError), or an open failure.
        onError: () => showToast('error'),
      });

      return (
        <button type="button" onClick={open}>
          Upgrade wallet
        </button>
      );
    }
    ```
  </Tab>
</Tabs>

## Hosting the app

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

1. **Dynamic-hosted** — use the shared Dynamic-hosted deployment as-is (the default; pass nothing). It lives at `https://waas-migration-dynamic-xyz.vercel.app`.
2. **Your own subdomain** — point a CNAME (e.g. `upgrade.yourapp.com`) at the Dynamic-hosted app (`waas-migration-dynamic-xyz.vercel.app`) so it runs on your own domain and branding, then reach out to Dynamic so we can add your custom domain to the deployment (otherwise it won't serve on your CNAME).
3. **Fork & self-host** — clone the [upgrade app repo](https://github.com/dynamic-labs-oss/waas-migration), build, and host it yourself.

For options 2 and 3, tell the package where the app lives with `baseUrl` — every entry point accepts it:

<Tabs>
  <Tab title="TypeScript">
    ```typescript theme={"system"}
    const baseUrl = 'https://upgrade.yourapp.com';

    // Redirect
    void startUpgradeRedirect({ baseUrl });

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

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

    // Low-level / React Native
    const uri = buildUpgradeUrl({ baseUrl });
    const unsubscribe = subscribeToUpgradeCompletion({ baseUrl, onComplete });
    ```
  </Tab>

  <Tab title="React">
    ```tsx theme={"system"}
    const baseUrl = 'https://upgrade.yourapp.com';

    // Embed hook
    const { upgradeUrl } = useUpgradeIframe({ active: isOpen, baseUrl, onComplete });
    ```
  </Tab>
</Tabs>

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

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

* **Your own subdomain / self-host** → add your origin (e.g. `https://upgrade.yourapp.com`).
* **Dynamic-hosted** → add `https://waas-migration-dynamic-xyz.vercel.app`.

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.

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

## Handling errors

| Situation                                      | What to do                                                                                                   |
| ---------------------------------------------- | ------------------------------------------------------------------------------------------------------------ |
| App served over http (not https)               | The upgrade app rejects non-https return URLs — serve your app over https.                                   |
| Outcome `status` is `error`                    | The upgrade failed or was cancelled — offer a retry.                                                         |
| Popup rejected with `UpgradePopupBlockedError` | The browser blocked the popup — open it synchronously from a click handler, with no `await` before the call. |
| Popup rejected with `UpgradePopupClosedError`  | The user closed the popup before finishing — offer a retry.                                                  |
| App calls blocked                              | The serving origin isn't allowlisted — add it to the environment's allowed origins.                          |

## See also

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