Skip to content

fix(headless): use Universal Link for custom-deeplink wallets on mobile#5675

Open
svenvoskamp wants to merge 2 commits into
mainfrom
svenvoskamp/solflare-mobile-deeplink
Open

fix(headless): use Universal Link for custom-deeplink wallets on mobile#5675
svenvoskamp wants to merge 2 commits into
mainfrom
svenvoskamp/solflare-mobile-deeplink

Conversation

@svenvoskamp
Copy link
Copy Markdown
Contributor

@svenvoskamp svenvoskamp commented May 21, 2026

Summary

In headless mode (useAppKitWallets().connect), tapping Solflare on mobile produced solflare://wc?uri=…, which Solflare's app does not handle — nothing opened. Phantom worked only by accident because its mobile_link happens to be a valid Universal Link base.

Two fixes:

  1. 0daaed468 — headless connect() now mirrors the headful selectWalletConnector behavior: for any wallet flagged by MobileWalletUtil.isCustomDeeplinkWallet, fire the Universal Link via handleMobileDeeplinkRedirect instead of building <mobile_link>wc?uri=….
  2. 4c9ec0a2eisCustomDeeplinkWallet and the matching branch in handleMobileDeeplinkRedirect were Solana-only for Solflare. They now also cover eip155. Solflare's in-app browser exposes both window.solflare (Solana) and window.ethereum (EVM), so the same https://solflare.com/ul/v1/browse/<dapp_url> URL is the right entry point either way.

How the bug happens (before fix)

flowchart TD
  A[user taps Solflare on mobile] --> B{isInjected && connector?}
  B -- yes --> X1[connectExternal]
  B -- no --> C{fallback connector?}
  C -- yes --> X2[connectExternal fallback]
  C -- no --> D{isMobile?}
  D -- yes --> E{wcWallet.mobile_link set?}
  E -- yes --> F["onConnectMobile<br/>builds solflare://wc?uri=..."]
  E -- no --> G[handleMobileDeeplinkRedirect<br/>Universal Link]
  F --> H["⛔ Solflare ignores<br/>solflare://wc?uri=..."]
  G --> I[✅ Solflare opens]

  style F fill:#ffd5d5,stroke:#c00
  style H fill:#ffd5d5,stroke:#c00
Loading

Solflare's WC-registry mobile_link is solflare://, so the headless flow always took the left branch and produced solflare://wc?uri=…. Solflare's app does not register a handler for that URL, so the redirect was a no-op.

Phantom's mobile_link is https://phantom.app/ul/v1/, so <mobile_link>wc?uri=… was coincidentally a valid Phantom Universal Link. That's why Phantom worked while Solflare didn't.

Headful vs. headless before any fix

sequenceDiagram
  autonumber
  participant U as User
  participant V as View / hook
  participant M as MobileWalletUtil
  participant C as ConnectionControllerUtil

  rect rgb(220,245,220)
  Note over U,C: Headful (worked)
  U->>V: tap Solflare
  V->>M: handleMobileDeeplinkRedirect(id, ns)
  M-->>U: window.location = https://solflare.com/ul/v1/browse/...
  Note over V: then RouterController.push(...)
  end

  rect rgb(255,220,220)
  Note over U,C: Headless (broken)
  U->>V: tap Solflare
  V->>C: onConnectMobile(wcWallet)
  C-->>U: window.location = solflare://wc?uri=...
  Note over U: ⛔ Solflare app ignores it
  end
Loading

The headful path always called handleMobileDeeplinkRedirect first (Universal Link). The headless path skipped that whenever mobile_link was set.

Fix #1 — mirror the headful behavior in headless connect()

flowchart TD
  A[user taps Solflare on mobile] --> B{isInjected && connector?}
  B -- yes --> X1[connectExternal]
  B -- no --> C{fallback connector?}
  C -- yes --> X2[connectExternal fallback]
  C -- no --> D{isMobile?}
  D -- yes --> NEW{{"isCustomDeeplinkWallet(id, ns)?<br/>NEW CHECK"}}
  NEW -- yes --> G[handleMobileDeeplinkRedirect<br/>Universal Link]
  NEW -- no --> E{wcWallet.mobile_link?}
  E -- yes --> F[onConnectMobile<br/>WC URI deeplink]
  E -- no --> G

  style NEW fill:#d5e6ff,stroke:#06c
  style G fill:#d5f5d5,stroke:#0a0
Loading

Custom-deeplink wallets (Phantom, Solflare, Coinbase, Binance) now skip the WC URI deeplink entirely and go straight to the Universal Link, matching headful.

Fix #2 — Solflare on EVM also needs the Universal Link

Before Fix #2, the allow-list inside MobileWalletUtil.isCustomDeeplinkWallet was:

Wallet Allowed namespaces (before)
Phantom solana eip155 bip122
Solflare solana
Coinbase solana eip155
Binance bip122

When BX has both the wagmi adapter and the Solana adapter, and the user taps Solflare while the active chain is eip155:

flowchart TD
  A[Solflare clicked<br/>activeChain = eip155] --> B{"isCustomDeeplinkWallet(SOLFLARE_ID, 'eip155')?"}
  B -- "false (before Fix #2)" --> C["onConnectMobile<br/>solflare://wc?uri=..."]
  C --> D[⛔ silently fails]

  B -- "true (after Fix #2)" --> E["handleMobileDeeplinkRedirect('eip155')"]
  E --> F[Universal Link<br/>https://solflare.com/ul/v1/browse/...]
  F --> G[✅ Solflare opens dApp<br/>in in-app browser]

  style C fill:#ffd5d5,stroke:#c00
  style D fill:#ffd5d5,stroke:#c00
  style E fill:#d5e6ff,stroke:#06c
  style F fill:#d5f5d5,stroke:#0a0
  style G fill:#d5f5d5,stroke:#0a0
Loading

The same Solana-only gate existed inside handleMobileDeeplinkRedirect itself, so it was double-locked. Both checks now allow solana | eip155.

Final allow-list

Wallet Allowed namespaces (after)
Phantom solana eip155 bip122
Solflare solana eip155 ← changed
Coinbase solana eip155
Binance bip122

End-to-end fixed flow

sequenceDiagram
  autonumber
  participant U as User (mobile)
  participant H as useAppKitWallets connect()
  participant M as MobileWalletUtil
  participant S as Solflare app

  U->>H: tap Solflare (activeChain = eip155 OR solana)
  H->>M: isCustomDeeplinkWallet(SOLFLARE_ID, ns) → true
  H->>M: handleMobileDeeplinkRedirect(SOLFLARE_ID, ns)
  M-->>U: window.location = https://solflare.com/ul/v1/browse/<dapp_url>?ref=<dapp_url>
  U->>S: iOS / Android opens Solflare via Universal Link
  S->>S: load dApp in in-app browser
  Note over S: window.solflare (Solana)<br/>+ window.ethereum (EVM) injected
  S-->>U: dApp connects via wagmi or wallet-standard
Loading

Test plan

  • pnpm --filter @reown/appkit-controllers test — full suite passing, including new cases for Solflare/Phantom on mobile across solana, eip155, and bip122 namespaces in tests/hooks/react.test.ts and tests/utils/MobileWallet.test.ts.
  • pnpm --filter @reown/appkit-controllers typecheck — clean.
  • Real-device check with canary 1.8.21-solflare-headless-deeplink-evm.0 in WalletConnect Pay buyer-experience — Solflare now opens on EVM-active and Solana-active flows; Phantom still opens; regular WC wallets still use the WC URI deeplink.

🤖 Generated with Claude Code

Solflare's mobile_link is solflare://, so the headless connect() was
building solflare://wc?uri=... which Solflare's app doesn't handle.
Route Phantom/Solflare/Coinbase/Binance through
MobileWalletUtil.handleMobileDeeplinkRedirect (their /ul/v1/browse/...
Universal Link) before falling through to the WC-URI deeplink. Matches
the headful selectWalletConnector behavior.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@changeset-bot
Copy link
Copy Markdown

changeset-bot Bot commented May 21, 2026

🦋 Changeset detected

Latest commit: 4c9ec0a

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 26 packages
Name Type
@reown/appkit-controllers Patch
@reown/appkit-adapter-bitcoin Patch
@reown/appkit-adapter-ethers Patch
@reown/appkit-adapter-ethers5 Patch
@reown/appkit-adapter-solana Patch
@reown/appkit-adapter-ton Patch
@reown/appkit-adapter-tron Patch
@reown/appkit-adapter-wagmi Patch
@reown/appkit Patch
@reown/appkit-ui Patch
@reown/appkit-core Patch
@reown/appkit-utils Patch
@reown/appkit-scaffold-ui Patch
@reown/appkit-siwe Patch
@reown/appkit-siwx Patch
@reown/appkit-wallet-button Patch
@reown/appkit-experimental Patch
@reown/appkit-pay Patch
@reown/appkit-cdn Patch
@reown/appkit-universal-connector Patch
@reown/appkit-testing Patch
@reown/appkit-common Patch
@reown/appkit-polyfills Patch
@reown/appkit-wallet Patch
@reown/appkit-cli Patch
@reown/appkit-codemod Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@vercel
Copy link
Copy Markdown

vercel Bot commented May 21, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
appkit-demo Ready Ready Preview, Comment May 21, 2026 10:52am
appkit-gallery Ready Ready Preview, Comment May 21, 2026 10:52am
appkit-headless-sample-app Ready Ready Preview, Comment May 21, 2026 10:52am
appkit-laboratory Ready Ready Preview, Comment May 21, 2026 10:52am
9 Skipped Deployments
Project Deployment Actions Updated (UTC)
appkit-basic-example Ignored Ignored May 21, 2026 10:52am
appkit-basic-sign-client-example Ignored Ignored May 21, 2026 10:52am
appkit-basic-up-example Ignored Ignored May 21, 2026 10:52am
appkit-ethers5-bera Ignored Ignored May 21, 2026 10:52am
appkit-nansen-demo Ignored Ignored May 21, 2026 10:52am
appkit-wagmi-cdn-example Ignored Ignored May 21, 2026 10:52am
ethereum-provider-wagmi-example Ignored Ignored May 21, 2026 10:52am
next-wagmi-solana-bitcoin-example Ignored Ignored May 21, 2026 10:52am
vue-wagmi-example Ignored Ignored May 21, 2026 10:52am

Request Review

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 21, 2026

Visual Regression Test Results ✅ Passed

⚠️ 15 visual change(s) detected

Chromatic Build: https://www.chromatic.com/build?appId=6493191bf4b10fed8ca7353f&number=924
Storybook Preview: https://6493191bf4b10fed8ca7353f-hnehilbyls.chromatic.com/

👉 Please review the visual changes in Chromatic and accept or reject them.

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 21, 2026

Warnings
⚠️

🌐 Non-company domain introduced (host: solflare.com) in .changeset/fix-solflare-evm-universal-link.md (line 5): https://solflare.com/ul/v1/browse/<dapp_url>

⚠️

🌐 Non-company domain introduced (host: solflare.com) in .changeset/fix-solflare-evm-universal-link.md (line 5): https://solflare.com/ul/v1/browse/<dapp_url>

⚠️

🌐 Non-company domain introduced (host: ) in packages/controllers/exports/react.ts (line 566): https://<wallet>/ul/v1/browse/<dapp_url>

⚠️

🌐 Non-company domain introduced (host: phantom.app) in packages/controllers/tests/hooks/react.test.ts (line 1792): https://phantom.app/ul/v1/

Generated by 🚫 dangerJS against 4c9ec0a

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 21, 2026

📦 Bundle Size Check

All bundles are within size limits

📊 View detailed bundle sizes

> @reown/appkit-monorepo@1.7.1 size /home/runner/work/appkit/appkit


> size-limit

@reown/appkit - Main Entry
Size limit:   80 kB
Size:         75.8 kB with all dependencies, minified and gzipped
Loading time: 1.5 s   on slow 3G
Running time: 1.5 s   on Snapdragon 410
Total time:   3 s
@reown/appkit/react
Size limit:   236 kB
Size:         235.04 kB with all dependencies, minified and gzipped
Loading time: 4.6 s     on slow 3G
Running time: 6.7 s     on Snapdragon 410
Total time:   11.3 s
@reown/appkit/vue
Size limit:   80 kB
Size:         75.8 kB with all dependencies, minified and gzipped
Loading time: 1.5 s   on slow 3G
Running time: 2.4 s   on Snapdragon 410
Total time:   3.9 s
@reown/appkit-scaffold-ui
Size limit:   220 kB
Size:         214.33 kB with all dependencies, minified and gzipped
Loading time: 4.2 s     on slow 3G
Running time: 2.8 s     on Snapdragon 410
Total time:   7 s
@reown/appkit-ui
Size limit:   500 kB
Size:         13.16 kB with all dependencies, minified and gzipped
Loading time: 258 ms   on slow 3G
Running time: 506 ms   on Snapdragon 410
Total time:   763 ms

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 21, 2026

Coverage Report

Status Category Percentage Covered / Total
🔵 Lines 78.6% 39952 / 50829
🔵 Statements 78.6% 39952 / 50829
🔵 Functions 76.18% 4265 / 5598
🔵 Branches 86.67% 9753 / 11252
File Coverage
File Stmts Branches Functions Lines Uncovered Lines
Changed Files
packages/controllers/exports/react.ts 90.8% 88.29% 46.15% 90.8% 71-80, 106-114, 131, 154, 555, 593-594, 146-172, 453-596
packages/controllers/src/utils/MobileWallet.ts 96.55% 95.23% 100% 96.55% 62, 66, 116, 126
Generated in workflow #17131 for commit 4c9ec0a by the Vitest Coverage Report Action

Solflare's in-app browser exposes both window.solflare and window.ethereum,
so the .../ul/v1/browse/<url> Universal Link is the right entry point for
EVM dApps too. Previously isCustomDeeplinkWallet and the redirect itself
both gated Solflare on namespace === SOLANA, so EVM-only consumers (no
Solana adapter registered) fell through to solflare://wc?uri=..., which
Solflare's app does not handle.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

npm start fails

2 participants