Implementing Embedded DApp Browser in Mobile Crypto Wallets
An embedded DApp browser is one of the most complex components in a crypto wallet. It must load arbitrary web applications, inject the window.ethereum provider, handle transaction signatures, and do all this without becoming a vector for attacks on user assets.
Architecture: WebView + Provider
The browser foundation is a native WebView with injected JavaScript provider. On iOS, this is WKWebView; on Android, WebView with addJavascriptInterface. MetaMask, Trust Wallet, and Coinbase Wallet implement this with the same pattern.
The window.ethereum provider is an object that DApps expect to find in the browser. It must implement the EIP-1193 interface: a request(method, params) method for all RPC requests.
Workflow:
DApp (JS) → window.ethereum.request({method: 'eth_sendTransaction'})
→ postMessage to native layer
→ native code shows user confirmation dialog
→ user approves/rejects
→ answer returned to JS via postMessage
→ Promise resolves in DApp
Provider Injection on iOS (WKWebView)
Inject the provider script via WKUserScript with injectionTime: .atDocumentStart. Critically, use atDocumentStart—otherwise the DApp may check for window.ethereum before injection and decide the wallet doesn't exist.
let providerScript = loadProviderJS() // Read from bundle
let userScript = WKUserScript(
source: providerScript,
injectionTime: .atDocumentStart,
forMainFrameOnly: false
)
webView.configuration.userContentController.addUserScript(userScript)
webView.configuration.userContentController.add(self, name: "ethereum")
The JS provider sends messages via webkit.messageHandlers.ethereum.postMessage({...}). Native code receives this in userContentController(_:didReceive:).
Return responses via webView.evaluateJavaScript("window.ethereum._resolveResponse(\(id), \(result))").
Android: addJavascriptInterface
webView.addJavascriptInterface(EthereumProvider(this), "AndroidEthereum")
webView.settings.javaScriptEnabled = true
On Android, JS interfaces work synchronously, creating a problem: @JavascriptInterface methods can't return Promises. Work around this using callbacks: JS calls AndroidEthereum.request(id, method, paramsJson), native code eventually calls webView.evaluateJavascript("resolveCallback($id, $result)", null).
Important: addJavascriptInterface is potentially dangerous. Methods annotated with @JavascriptInterface are visible to all JavaScript on the page, including malicious iframes. Only annotate necessary methods. Never add a broad API interface.
Implementing the window.ethereum Provider
A minimal implementation supports EIP-1193 methods:
// Injected provider (simplified)
window.ethereum = {
isMetaMask: true, // many DApps check this flag
chainId: '0x1',
selectedAddress: null,
request: async function({ method, params }) {
return new Promise((resolve, reject) => {
const id = generateId();
pendingRequests[id] = { resolve, reject };
webkit.messageHandlers.ethereum.postMessage({ id, method, params });
});
},
on: function(event, handler) {
// chainChanged, accountsChanged, connect, disconnect
eventHandlers[event] = eventHandlers[event] || [];
eventHandlers[event].push(handler);
}
};
Methods to support:
-
eth_requestAccounts— request account access, show dialog -
eth_accounts— list connected addresses -
eth_chainId— current network -
eth_sendTransaction— send transaction, requires confirmation -
personal_sign— sign message -
eth_signTypedData_v4— sign structured data (EIP-712) -
wallet_switchEthereumChain— request network switch
Security: The Top Priority
Session Isolation. Each DApp must have separate cookies and localStorage. Don't allow DApp A to read DApp B's data. On iOS, use separate WKWebViewConfiguration and WKWebsiteDataStore per tab.
URL Verification Before Injection. Don't inject the provider on arbitrary pages—only on HTTPS, only on DApp domains from your whitelist, or with explicit user consent.
Transaction Confirmation Dialog. Users must see: contract address, ETH amount (if any), transaction data in human-readable form (decoded via ABI), gas estimate, and total cost in fiat. Don't show raw hex.
eth_signTypedData_v4 (EIP-712). This is structured data—the DApp asks to sign a typed object. Parse the JSON schema and show users exactly what they're signing in human-readable form. MetaMask does this by parsing the types and message fields. Blind signing is a risk.
Phishing Protection. Verify SSL certificates, display the URL in an address bar users can't hide, block alert() and prompt() from JS (DApps shouldn't hijack native dialogs). On iOS, handle webView(_:runJavaScriptAlertPanelWithMessage:) and replace with native UIAlertController.
Multi-tab and History
A single-tab browser is the minimum. Implement:
- Tabs with isolated data
- Browse history (optional; many wallet users prefer privacy)
- Bookmarks for frequently used DApps
- Curated popular DApp list for onboarding
On iOS, keep multiple WKWebView instances in memory—they're lazy until visible. On Android, WebView is heavy; for memory savings, destroy the inactive tab's WebView and restore the URL on return.
React Native and Flutter
In React Native, use react-native-webview with injection via injectedJavaScriptBeforeContentLoaded and onMessage. Limitation: injectedJavaScriptBeforeContentLoaded on Android isn't synchronous—there's a race condition. Solution: duplicate injection via evaluateJavaScript in onLoadStart.
In Flutter, use webview_flutter with addJavaScriptChannel and runJavaScript. Similar limitations on Android.
Performance
WebView renders full web content—this is resource-intensive. On older Android devices (Chromium 80-based WebView), complex DeFi DApps may stutter. Monitor via WebViewClient.onPageStarted / onPageFinished, show a progress bar.
Preload WebView at app startup (create an instance in the background) reduces cold-start browser time from ~800ms to ~200ms.
Development Process
- Basic WebView with navigation, address bar, progress bar
-
Provider injection and support for
eth_requestAccounts,eth_accounts,eth_chainId - Transaction dialog — display, signing, sending via RPC
-
Advanced methods —
personal_sign,eth_signTypedData_v4,wallet_switchEthereumChain - Security — isolation, phishing protection, security audit
- Multi-tab and UX polish
Timeline: basic browser with eth_sendTransaction—3–4 weeks. Full-featured browser with multi-tab, EIP-712 display, security audit—2–3 months.







