Skip to content

Opening a dApp inside Solana mobile wallets

A React dApp built on @solana/wallet-adapter-react connects desktop wallets without trouble, but on a phone the same flow falls apart: the wallet has to open the dApp inside its own in-app browser, and the deep links that should make that happen quietly do not. Backpack lands on a “download the app” page; Solflare opens the app but never the site; every variant seems to fail. We took the problem apart, and it turned out to be four separate problems wearing one symptom.

The four problems

  • The Backpack link was malformed. The only documented format is https://backpack.app/ul/v1/browse/<url>?ref=<ref> — a universal link with the target URL in the path and a required ref. A custom-scheme guess like backpack://ul/v1/browse?url=... matches no route the app registers, so the user ends on the wallet’s install page.
  • Solflare needs its universal link too: https://solflare.com/ul/v1/browse/<url>?ref=<ref>, not the bare solflare:// scheme. A bare scheme can launch the app without routing it — which is exactly “the app opens, but the site tab has to be opened by hand”.
  • Both parameters must be encoded. url is the full absolute dApp address and ref is the requesting origin, each passed through encodeURIComponent. An unencoded ? or & in the target corrupts the parse, and the wallet opens on its home screen instead of the browser tab.
  • The trigger matters as much as the link. Universal links only switch apps on a navigation the operating system trusts — and they deliberately do nothing when pasted into the address bar, which is also how a perfectly correct link “fails” during testing.

The documented formats

  • Phantom: https://phantom.app/ul/browse/<url>?ref=<ref> — no /v1 in this one.
  • Solflare: https://solflare.com/ul/v1/browse/<url>?ref=<ref>
  • Backpack: https://backpack.app/ul/v1/browse/<url>?ref=<ref>

One pattern serves all three:

const WALLET_BROWSE = {
  phantom: (url, ref) =>
    `https://phantom.app/ul/browse/${url}?ref=${ref}`,
  solflare: (url, ref) =>
    `https://solflare.com/ul/v1/browse/${url}?ref=${ref}`,
  backpack: (url, ref) =>
    `https://backpack.app/ul/v1/browse/${url}?ref=${ref}`,
};

function walletBrowseLink(
  walletName,
  targetUrl = window.location.href,
) {
  const build = WALLET_BROWSE[walletName.toLowerCase()];
  if (!build) return null;
  return build(
    encodeURIComponent(targetUrl),
    encodeURIComponent(window.location.origin),
  );
}
  • Render a real anchor, precomputed. A plain <a href={walletBrowseLink('phantom')}> is the most reliable trigger on both platforms.
  • If it must be programmatic, assign window.location.href synchronously inside the tap handler — no await, no fetch, no setTimeout first. After asynchronous work the gesture context is gone, and iOS falls back to the wallet’s website. Never window.open.
  • Never test by pasting into the address bar. Universal links deliberately do not fire there; test with a tapped link or a QR code scanned by the camera.
  • Mind the messenger webviews. Opened inside Telegram’s or Instagram’s in-app browser, universal links are frequently swallowed and the wallet’s plain website loads instead. User-agent detection is heuristic at best, so also give users a visible escape hatch: “open in Safari or Chrome, then connect”.

The bigger fix on Android

Hand-rolled deep links are the iOS story. On Android, Solana Mobile’s Mobile Wallet Adapter lets a dApp running in the mobile browser connect straight to the installed wallet app, with no in-app-browser detour at all. Recent versions of @solana/wallet-adapter-react register the mobile adapter automatically, so upgrading the wallet-adapter packages can fix Android by itself. The target architecture: Mobile Wallet Adapter on Android, browse universal links on iOS, where Apple allows no equivalent.

Verifying on a device

  1. Real device, wallet installed, link opened from the system browser — not from a messenger.
  2. Tap a rendered link or scan a QR code; never paste into the address bar.
  3. Confirm the wallet opens and the dApp loads in its in-app browser tab — the second half is the part that fails.
  4. Repeat without the wallet installed: the universal link should degrade to the wallet’s website. That page appearing while the app is installed means the link or the trigger is still wrong.
  5. Then test the messenger path, and add the “open in browser” hint if it fails there.

Sources

This is the kind of problem we untangle for clients. Get in touch.

All notes