Skip to main content
This is an enterprise-only feature. Please contact us to enable.
The same headless architecture as iOS: render your own native list, and drive a hidden WebView that runs the Dynamic SDK and returns results (including message and transaction signatures) over a JS bridge. No wallet SDK in the app. The basic Android flow is the recommended default.
No SDK in your app. Your app links no wallet SDK. It needs a hidden WebView pointed at the hosted engine page (https://connect.dynamicauth.com/headless.html, set as ENGINE_URL in FireblocksHeadlessConnect) and your URL scheme. All WalletConnect / MetaMask / Phantom logic (and the wallet list) comes from that hosted view. Same bridge contract as iOS.
Copy FireblocksHeadlessConnect.kt (below) and FireblocksConnect.kt from the basic Android guide (visible fallback).

1. Get the wallet menu (no static file)

The engine derives the list live from the Dynamic catalog and pushes it over the bridge (a wallets message). Set FireblocksHeadlessConnect.onWallets.

2. Drop in FireblocksHeadlessConnect

Owns a hidden WebView, bridges to it (addJavascriptInterface + evaluateJavascript), 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.
MainActivity.kt
FireblocksHeadlessConnect.kt

3. Sign a message

The hosted engine supports window.headlessConnect.sign. After a successful connect, invoke sign() with any string. The wallet app prompts the user; the callback delivers a hex signature.
MainActivity.kt
Only available for wallets connected through the headless engine. Visible-flow wallets do not hold an open session.

4. Sign a transaction

Pass a serialized transaction. Signing only, no broadcast.
MainActivity.kt (EVM)
MainActivity.kt (Solana)

5. Wire the manifest and redirect

Beyond the basic flow, headless needs two manifest additions: a phantom-headless host on the same FireblocksRedirectActivity intent-filter, and a <queries> block so the app can open wallet deeplinks on Android 11+.
AndroidManifest.xml
Route the return activity to the engine first, then fall through to the visible flow. The engine only consumes <scheme>://phantom-headless, which carries no data: Phantom connects inside its own in-app browser, the engine receives the connection over the WalletConnect relay, and the link only brings your app forward.
FireblocksRedirectActivity
The hidden WebView needs the INTERNET permission. Do not call webView.onPause() on it. That suspends the relay socket.

6. Render the list (your UI)

MainActivity is a sample list (search, chain picker, connecting state, auto-fallback) you would swap for your own design.
MainActivity.kt

7. Connect Phantom

Connect Phantom on EVM or Solana with the same call as any other wallet: FireblocksHeadlessConnect.connect(this, "phantom", "evm"), or "solana" as the chain. 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. FireblocksHeadlessConnect.handleReturnURL (called from FireblocksRedirectActivity) 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.

8. 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.
FireblocksWalletBrowser.kt

Carry the two extra fields

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

Register the callback host

Add a third host to the FireblocksRedirectActivity intent-filter and route it before the Custom Tab flows, which all share wallet-callback.
AndroidManifest.xml
FireblocksRedirectActivity

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.
MainActivity.kt
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

MainActivity.kt

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.

9. The bridge (for reference)

Identical to iOS.
bridge messages

Common pitfalls

  • Do not call webView.onPause() on the hidden WebView. That suspends its relay socket.
  • Cancellation is not auto-detected by Custom Tabs. Treat resumed-with-no-result as cancelled, or hold the pending flow in a ViewModel.
  • Test on a device with a wallet installed, not a bare emulator.
  • Serve over HTTPS. The flow mints WalletConnect URIs via WebCrypto.
Last modified on October 1, 2026