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.
Connect.swift
Forward your app’s onOpenURL to FireblocksHeadlessConnect.shared.handleReturnURL($0). That is how redirect wallets (Phantom) hand their result back to the hidden view.
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 on EVM (its own browser)

Phantom injects an EVM provider (window.phantom.ethereum) only inside its own in-app browser. It has no WalletConnect entry in Dynamic’s wallet book and no EVM deeplink, so the hidden engine cannot drive it and the visible flow has no provider to talk to. The route that works is to open your hosted page inside Phantom’s browser and take the result back over your URL scheme. 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 a normal chain row (“Ethereum & EVM”, with “Opens in the wallet’s own browser” underneath). The synthetic value stays in your UI and is never sent to the page.

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.

7. 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 September 21, 2026