Skip to main content
This is an enterprise-only feature. Please contact us to enable.
Want your app to render its own native wallet list with no web UI? Keep connection logic in the web layer and drive it from a hidden web view. The basic iOS flow is the recommended default; use headless when you need a fully native front end. After connecting, you can also sign messages and transactions over the bridge.
No SDK in your app. Your app links no wallet SDK: no CocoaPods, no native crypto. It needs a hidden web view pointed at the /headless.html engine route, and your URL scheme. All WalletConnect / MetaMask / Phantom logic (and the wallet list itself) comes from that hidden view. When wallets or the SDK change, you redeploy the page; the app never changes.
Copy FireblocksHeadlessConnect.swift (below) and FireblocksConnectFlow.swift from the basic iOS guide (visible fallback and shared WalletConnection).

1. Get the wallet menu (no static file)

The engine derives the list live from the Dynamic catalog and pushes it to your app over the bridge (a wallets message). No walletbook file to ship or keep in sync.
wallets message (web to app)

2. Drop in FireblocksHeadlessConnect

One file owns a hidden WKWebView pointed at /headless.html, drives it over a message bridge, opens the wallet deeplink it returns, and calls you back. Pre-warm it at launch. Set environmentId before prewarm() to target a different Dynamic environment, and locale / theme the same way to override the engine’s UI locale and theme; all three are forwarded to the iframe app as query parameters.
Connect.swift
Forward your app’s onOpenURL to FireblocksHeadlessConnect.shared.handleReturnURL($0). It consumes <scheme>://phantom-headless, the link Phantom opens to bring your app back after a connection (see Connect Phantom).
FireblocksHeadlessConnect.swift

3. Sign a message

The hosted engine supports window.headlessConnect.sign. After a successful headless connect, call sign() with any string. The wallet app prompts the user to approve; the callback delivers a hex signature (EVM) or base58 (Solana).
SignView.swift
Signing is only available for wallets connected through the headless engine. Wallets that completed the visible fallback flow do not hold an open session.

4. Sign a transaction

Pass a serialized transaction to signTransaction() (engine: signTx). This only signs. It does not broadcast. Format and return value differ by chain.
SignView.swift (EVM)
SignView.swift (Solana)

5. Render the list (your UI)

On tap, route to the engine (mode: "headless") or the visible flow (mode: "fallback"). The engine also returns .fallbackRequired for anything it cannot do silently, so you fall back automatically. WalletListView is a sample you would swap for your own design.
WalletListView.swift

6. Connect Phantom

Connect Phantom on EVM or Solana with the same call as any other wallet: connect(walletKey: "phantom", chain: "evm") or chain: "solana". Phantom has no WalletConnect entry in Dynamic’s wallet book, so the engine bridges through Phantom’s own in-app browser:
  1. The engine mints a WalletConnect pairing and sends a deeplink that opens your hosted page inside Phantom.
  2. The page approves the pairing with Phantom’s injected provider, and the engine receives the connection over the WalletConnect relay.
  3. Phantom’s “Return to app” button opens <scheme>://phantom-headless with no payload. handleReturnURL consumes it, and your connect callback receives the result from the engine.
No connection data comes back through your URL scheme. The engine offers a chain only when your Dynamic environment has a network enabled for it, and it fails with timeout if Phantom does not approve within 60 seconds.

7. Use a wallet’s own browser (Phantom on EVM)

Phantom injects its EVM provider (window.phantom.ethereum) only inside its own in-app browser. To sign or send with it, open your hosted page inside Phantom’s browser: connect there, then reopen the same browser for each sign or send. Each operation is one round trip: Phantom comes to the foreground with your page in it, the user approves, and the result arrives on <scheme>://wallet-browser.
The engine reports each wallet’s in-app-browser template in the wallets bridge message as inAppBrowser. The template contains {{encodedDappURI}}, and you replace every occurrence (Phantom’s uses it twice). A template is not a chain: it only means the wallet can open a URL in its own browser, so you decide per wallet which chains that browser serves.
FireblocksWalletBrowserFlow.swift

Carry the two extra fields

Add the template to HeadlessWallet and remember it on the connection, or sign and send fall back to the engine.

Forward the callback

Your existing CFBundleURLTypes entry already covers <scheme>://wallet-browser, so there is nothing to register per host. Hand the URL to the flow after the engine. No ASWebAuthenticationSession is involved in this return, so nothing else picks it up.
App.swift
If you run the flow inside your own WKWebView, catch the <scheme>://wallet-browser navigation there and pass it to the same method.

Offer the option only for Phantom

Do not derive EVM support from the presence of a template. Phantom’s template comes from its Sui wallet-book entry, so a template on its own says nothing about EVM. The evidence for Phantom specifically is phantomevm.injectedConfig.windowLocations: ["phantom.ethereum"], an EIP-1193 provider inside its browser.
WalletListView.swift
Show it as its own chain row with “Opens in the wallet’s own browser” underneath. The current engine already lists evm in Phantom’s chains and connects it headlessly (Connect Phantom), so the !wallet.chains.contains("evm") guard keeps the synthetic row from doubling it: it only appears for engines that predate the headless bridge, or if you want the row to route into the wallet-browser connect below instead, which is what signing needs. A headless Phantom connection is connect-only.

Connect, sign, and send

What travels on the URL

The callback carries address and chain (connect), signature (sign), or txHash (send), or error=1&code=&message=, always with the nonce echoed back. A send is already broadcast when the hash arrives. One request is in flight at a time, a new one supersedes the previous, and an abandoned one times out after five minutes.

Phantom pitfalls

  • Keep the template on the connection. Sign and send must reopen the same browser, because the account exists nowhere else. Losing the stored template sends the request to the engine, which reports no wallet connected.
  • Use a separate callback host. wallet-callback is claimed by the visible flow, so a link arriving from Phantom would be dropped or complete an unrelated request.
  • Watch where the template lands on Android. The template is an https app link and reaches Phantom only if its app links are verified. Otherwise Android can hand it to Chrome, where nothing is injected and the page correctly reports no EVM path. That message in a browser that is not Phantom means the hand-off went to the wrong app.
  • Offer the return anchor. The page renders a “Return to the app” link for browsers that ignore a programmatic redirect.

8. The bridge (for reference)

You do not write the bridge. It is what flows between the hidden view and the harness files. Handy when debugging.
bridge messages
The component mints requestId as a fresh UUID per connect and drops any message whose id does not match the one still in flight, so a stale or forged reply cannot complete a newer request. It also drops anything posted from an origin other than the engine URL, and rejects a connected message without a non-empty address as malformed_result.

Common pitfalls

  • Keep the hidden web view in the hierarchy. A fully detached WKWebView gets suspended by iOS and its relay socket stalls. Keep it 1×1 and hidden.
  • Test on a physical device. Wallets do not run in the Simulator.
  • Serve over HTTPS. The flow mints WalletConnect URIs via WebCrypto, which needs a secure context.
  • Native deeplinks are faster. Universal links round-trip through the wallet’s link server first; the hosted page prefers native schemes when embedded.
Last modified on October 1, 2026