web3_webview 1.0.14 copy "web3_webview: ^1.0.14" to clipboard
web3_webview: ^1.0.14 copied to clipboard

web3_webview is a powerful bridge between a DApp running inside a WebView and your Flutter application, enabling secure and seamless two-way communication between them

web3_webview #

web3_webview is a Flutter bridge between a DApp running inside an InAppWebView and your host app. It injects an EIP-1193 / EIP-6963 compliant window.ethereum provider (via ethers.min.js + generated script) and routes all wallet operations – connect, sign, sendTransaction, chain management – to native Dart dialogs and web3dart.

Security-first fork: private-key validation, XSS-safe JS injection, EIP-1193 error codes, PermissionRequest default-deny, non-blocking eth_sendTransaction, and correct EIP-712 padding. See Security.


Architecture #

flowchart LR
  DApp -- window.ethereum.request --> JSBridge[JS Provider\nprovider_script.dart]
  JSBridge -- flutter_inappwebview callHandler\nethereumRequest --> WebView[Web3WebView\nweb3_webview_eip1193.dart]
  WebView --> Provider[EthereumProvider\nsingleton + Web3Client]
  Provider --> Signer[Web3Signer\nPrivateKeySigner | external]
  Signer --> Signing[SigningHandler\nEIP-191 / EIP-712]
  Signer --> Tx[TransactionHandler\nsend + estimate + gas]
  Provider --> Dialog[WalletDialogService\nbottom sheet]
  Provider -- jsonEncode + evaluateJavascript --> JSBridge

Core modules:

Module File Responsibility
Web3WebView lib/web3_webview_eip1193.dart Wraps flutter_inappwebview, injects ethers.min.js + provider script at AT_DOCUMENT_START (Android re-inject on onLoadStart), supports read-only when no signer, forwards onPermissionRequest as DENY by default
EthereumProvider lib/ethereum/ethereum_provider.dart Singleton, Web3Client + Client lifecycle, handleRequest dispatcher, 12s eth_blockNumber cache, chain registry, event emit via jsonEncode. New: Web3Signer? abstraction – PrivateKeySigner / external / null (read-only)
Web3Signer lib/signer/web3_signer.dart Abstract address, signMessage(method,from,msg,pw), sendTransaction(tx); implement for WalletConnect / Secure Enclave etc.
PrivateKeySigner lib/signer/private_key_signer.dart Local EthPrivateKey impl via SigningHandler + TransactionHandler, auto-updates on chain switch via getters
ProviderScriptGenerator lib/provider/provider_script.dart Generates JS EthereumProvider extends EventTarget, jsonEncode-escapes chainId/accounts/isConnected/info, handles request/send/sendAsync, EIP-6963 announce
SigningHandler lib/signing/signing_handler.dart personal_sign/eth_sign/eth_signTypedData* with full EIP-712 v3/v4 validation, struct hashing, 32-byte padding for bool/address/bytesN
TransactionHandler lib/transaction/transaction_handler.dart Validate to/value/data, estimateGas + 20% BigInt buffer, signTransaction(chainId), sendRawTransaction returns hash immediately (EIP-1193), optional waitForConfirmation
WalletDialogService lib/ethereum/wallet_dialog_service.dart Bottom sheets for connect/sign/tx/switch/add, requestFrom = controller.getUrl().host, WalletDialogTheme

Key Features #

  • Wallet Connection – user-confirmed eth_requestAccounts; returns [] on eth_accounts until connected
  • DApp Integrationwindow.ethereum + window.web3.currentProvider, isMetaMask=true, EIP-6963 discovery
  • Read-only mode – omit privateKey/signer and DApp still loads; eth_call, eth_getBalance, eth_blockNumber etc. work, signing throws 4100
  • External signer – implement Web3Signer for WalletConnect, Secure Enclave, biometrics – no private key in RAM
  • Transaction Lifecycle – confirmation dialog, eth_estimateGas buffered, non-blocking hash return, fire-and-forget receipt polling (opt-in blocking)
  • Message Signingpersonal_sign (EIP-191 prefix), eth_sign (raw keccak), eth_signTypedData/v1/v3/v4 (EIP-712)
  • Chain Managementwallet_switchEthereumChain/wallet_addEthereumChain with idempotency checks
  • Two-way Bridgeflutter_inappwebview callHandlerevaluateJavascript, XSS-safe via jsonEncode
  • Multi-chain – any EVM via NetworkConfig

Requirements #

  • Flutter >=3.24.0, Dart ^3.5.0
  • flutter_inappwebview ^6.1.5, web3dart ^2.7.3

Installation #

dependencies:
  web3_webview: ^1.0.11
import 'package:web3_webview/web3_webview.dart';

Quick Start #

privateKey is now optional. If omitted and no signer is provided, the WebView runs in read-only mode (only RPC reads succeed; signing throws 4100). signer takes precedence over privateKey.

Do not hardcode keys. Load from flutter_secure_storage / Keychain / Keystore and clear on lock. Or use Web3Signer to avoid storing keys at all. See Security.

A. Read-only (no signer) #

Web3WebView(
  web3WalletConfig: Web3WalletConfig(
    currentNetwork: ethMainnet,
    supportNetworks: [ethMainnet, bscMainnet],
    name: 'MyDApp',
  ),
  initialUrlRequest: URLRequest(url: WebUri('https://metamask.github.io/test-dapp/')),
)
// DApp loads, eth_call/eth_getBalance work, eth_sendTransaction -> {code:4100}

B. Local private key (legacy, still supported) #

import 'package:flutter_secure_storage/flutter_secure_storage.dart';

class DappScreen extends StatefulWidget {
  const DappScreen({super.key});
  @override State<DappScreen> createState() => _DappScreenState();
}
class _DappScreenState extends State<DappScreen> {
  String? _pk;
  @override void initState() {
    super.initState();
    const storage = FlutterSecureStorage();
    storage.read(key: 'evm_pk').then((v) => setState(() => _pk = v));
  }
  @override Widget build(BuildContext context) {
    if (_pk == null) return const Center(child: CircularProgressIndicator());
    return Web3WebView(
      web3WalletConfig: Web3WalletConfig(
        privateKey: _pk!, // 0x + 64 hex, validated -32602 on invalid
        currentNetwork: ethMainnet,
        supportNetworks: [ethMainnet, bscMainnet],
        name: 'MyDApp Wallet',
        id: 'com.example.mydapp',
        dialogTheme: WalletDialogTheme(primaryColor: Color(0xFF3B82F6)),
        onError: (method, params, message) => debugPrint('[$method] $message'),
      ),
      initialUrlRequest: URLRequest(url: WebUri('https://metamask.github.io/test-dapp/')),
      onPermissionRequest: (ctrl, req) async => PermissionResponse(
        resources: req.resources, action: PermissionResponseAction.DENY,
      ),
    );
  }
}
import 'package:web3_webview/web3_webview.dart';

class WalletConnectSigner extends Web3Signer {
  final String _addr;
  WalletConnectSigner(this._addr);
  @override String get address => _addr;
  @override Future<String> signMessage(String method, String from, dynamic message, String pw) {
    // delegate to WalletConnect / enclave
    return walletConnect.sign(method, from, message);
  }
  @override Future<String> sendTransaction(Map<String, dynamic> tx) {
    return walletConnect.sendTransaction(tx);
  }
}

Web3WebView(
  web3WalletConfig: Web3WalletConfig(
    signer: WalletConnectSigner('0xAbc...'),
    currentNetwork: ethMainnet,
  ),
  initialUrlRequest: URLRequest(url: WebUri('https://app.uniswap.org/')),
)

Handling Web3WalletConfig #

Field Type Required Notes
privateKey String? no* 0x + 64 hex or 64 hex; validated with -32602 on invalid. *Required only for local signing; omit for read-only / external signer
signer Web3Signer? no* external signer (WalletConnect etc.), takes precedence over privateKey. If both null → read-only
currentNetwork NetworkConfig? no defaults to 0x1
supportNetworks List<NetworkConfig>? no defaults to [eth, bsc]
name/icon/id String? no EIP-6963 info (icon is data-URI), idrdns
dialogTheme WalletDialogTheme? no colors, text styles, paddings
onError void Function(JsonRpcMethod, List?, String)? no invoked before JS error is thrown, receives EIP-1193 code in message

NetworkConfig { chainId (0x hex), chainName, nativeCurrency?, rpcUrls, blockExplorerUrls? }

Read-only check: config.isReadOnly / config.hasSigner; provider: EthereumProvider().isReadOnly.


Signer & Read-only mode #

lib/signer/web3_signer.dart:14:

abstract class Web3Signer {
  String get address; // EIP-55
  Future<String> signMessage(String method, String from, dynamic message, String password);
  Future<String> sendTransaction(Map<String, dynamic> txParams); // -> tx hash
  Future<Uint8List> signTransaction(Transaction tx, int chainId) => throw UnsupportedError(...);
}
  • Read-only: Web3WalletConfig() with no privateKey/signerEthereumProvider.isReadOnly==true, window.ethereum.isConnected==false, eth_accounts==[]. eth_call, eth_getBalance, eth_blockNumber, eth_chainId, eth_estimateGas, wallet_switchEthereumChain etc. still work via Web3Client. Signing methods throw 4100 (unauthorized) which surfaces as code:4100 in DApp.
  • PrivateKey: PrivateKeySigner lib/signer/private_key_signer.dart:11 is auto-created from privateKey; uses SigningHandler/TransactionHandler internally and auto-updates chainId/Web3Client on network switch.
  • External: implement Web3Signer and pass Web3WalletConfig(signer: ...). Dialogs (showConnectWallet/showSignMessage/showTransactionConfirm) are still shown by EthereumProvider before delegating; your signer only does crypto/RPC.

Supported JSON-RPC #

EthereumProvider.handleRequest (lib/ethereum/ethereum_provider.dart:189) dispatches:

Method Params Behaviour
eth_requestAccounts [] Bottom sheet → connect + accountsChanged events, returns [address] or 4001; throws 4100 in read-only
eth_accounts [] [] until connected (or read-only)
eth_chainId [] state.chainId (hex)
net_version [] decimal chainId
eth_blockNumber [] 12s cached HexUtils.numberToHex(blockNumber)
eth_call [tx, block] makeRPCCall('eth_call') (no signing)
eth_sendTransaction [tx] Dialog → signer.sendTransactionhash immediately, polling fire-and-forget; 4100 in read-only
eth_getBalance [addr, tag] getBalance → hex
eth_getBlockByNumber/Hash [id, withTx] makeRPCCallMap?
eth_getTransactionByHash/Receipt [hash] makeRPCCallMap?
eth_getCode/StorageAt/TransactionCount [...] getCode/getStorage/getTransactionCount or RPC fallback
eth_gasPrice / eth_estimateGas []/[tx] hex; estimate buffered +20% via BigInt (works in read-only)
personal_sign [hexMsg, addr, pw?] dialog → signer.signMessage; 4100 in read-only
eth_sign [addr, hexMsg] raw keccak256; 4100 in read-only
eth_signTypedData[*_v1/v3/v4] [addr, typed] EIP-712; 4100 in read-only
wallet_switchEthereumChain [{chainId}] idempotent, dialog if needed → chainChanged (works in read-only)
wallet_addEthereumChain [NetworkConfig] dialog → add + auto-switch
wallet_getPermissions [] ['eth_accounts','eth_chainId','personal_sign']
wallet_revokePermissions [] true
other throws UnsupportedMethodException(4200)

Unknown method surfaces to JS as {code:4200, message} and to onError.

Error codes (lib/exceptions.dart:2) #

WalletException.code follows EIP-1193 + JSON-RPC:

  • 4001 – user rejected
  • 4100 – unauthorized (no signer / read-only)
  • 4200 – unsupported method
  • 4900 – disconnected
  • -32601 – method not found
  • -32602 – invalid params
  • -32603 – internal

Web3WebView (lib/web3_webview_eip1193.dart:825) maps WalletException.code into the thrown {code,message} consumed by provider_script.dart:127 _processError.


Permissions & WebView #

  • onPermissionRequest defaults to DENY (lib/web3_webview_eip1193.dart:1019). Override Web3WebView.onPermissionRequest to grant selectively, never GRANT all.
  • shouldInterceptRequest/onReceivedServerTrustAuthRequest are forwarded; implement cert pinning in host if needed.
  • Provider injected as two UserScript at AT_DOCUMENT_START; on Android re-evaluated in onLoadStart.

Signing details (lib/signing/signing_handler.dart:1) #

  • personal_sign accepts 0x hex or UTF-8, decodes for UI, signs prefixed.
  • eth_sign signs keccak256(hexOrUtf8) directly (dangerous – dialog still shown).
  • Typed data: types must contain EIP712Domain; checks primaryType, domain whitelist (name,version,chainId,verifyingContract,salt), circular deps, array lengths, and ABI 32-byte padding for bool/address/bytesN/uint/int.

Transaction details (lib/transaction/transaction_handler.dart:1) #

  • Requires to (0x 42 chars), validates value/data as 0x hex.
  • getTransactionCount(from) as nonce, estimateGas +20% with BigInt (*120/100), getGasPrice() or gasPrice override.
  • handleTransaction({waitForConfirmation=false}) – default returns hash; set true to block up to 30×10s polling and throw on receipt.status==false or timeout.

Theming (light + auto dark) #

// Light (default)
final light = WalletDialogTheme(primaryColor: Color(0xFF6366F1));
// Dark – auto-used when Theme.brightness == dark, or pass custom
final dark = WalletDialogTheme.dark(primaryColor: Color(0xFF818CF8));

Web3WalletConfig(
  dialogTheme: light,
  darkDialogTheme: dark, // optional – if null auto-derived from light
)
  • Auto dark: WalletDialogService checks Theme.of(context).brightness – no extra code needed; BottomSheetDialog barrier 0.42, cards adapt (surface #1E293B, border #334155, text #F1F5F9).
  • Premium redesign (1.0.12): 20 radius, 16 card, handle 36x4, origin chip with avatar, hero amount card, warning amber/red, FilledButton 52h/14 radius + OutlinedButton, ExpansionTile hex.
  • Custom per-dialog still via WalletDialogTheme(primaryColor:…, borderRadius:…), consumed by WalletDialogService (lib/ethereum/wallet_dialog_service.dart:7).

Try the example: AppBar dark toggle → all eth_requestAccounts / personal_sign / eth_sendTransaction dialogs switch instantly.

Custom UI (fully replace any dialog) #

You can keep the premium dialogs (theming only) or completely replace them to match your app branding – no fork needed.

// 1. Define builders once (or per-WebView)
final myBuilders = WalletDialogBuilders(
  connect: (ctx, {required address, required host, required appName, required controller, required theme}) async {
    return showDialog<bool>(
      context: ctx,
      barrierDismissible: false,
      builder: (c) => AlertDialog(
        shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
        title: Text('$appName muốn kết nối', style: theme.headerStyle),
        content: Text('Host: $host\nWallet: $address'),
        actions: [
          TextButton(onPressed: () => Navigator.pop(c, false), child: Text('Từ chối')),
          FilledButton(onPressed: () => Navigator.pop(c, true), style: FilledButton.styleFrom(backgroundColor: theme.primaryColor), child: Text('Kết nối')),
        ],
      ),
    );
  },
  // sign, transaction, switchNetwork, addNetwork – same pattern
  sign: (ctx, {required message, required address, required host, required controller, required theme}) => ...,
  transaction: (ctx, {required txParams, required host, required controller, required theme}) => ...,
);

// 2. Pass via config (per-WebView) – takes precedence
Web3WebView(
  web3WalletConfig: Web3WalletConfig(
    privateKey: pk,
    dialogTheme: light,
    darkDialogTheme: dark,
    dialogBuilders: myBuilders, // <-- all dialogs now use your widgets
  ),
)

// 3. Or globally: WalletDialogService.instance.setBuilders(myBuilders);
  • What you receive: BuildContext (for showDialog/Navigator), host, address/message/txParams, controller (for getUrl()), and the effective WalletDialogTheme (already resolved for light/dark + your custom colors).
  • What to return: Future<bool?>true = confirm, false/null = reject/cancel (maps to EIP-1193 4001).
  • Fallback: any null builder falls back to the premium default; you can override only connect and keep sign premium.
  • Theming still works: your custom widget can use theme.primaryColor / textColor / borderColor to stay consistent with dialogTheme/darkDialogTheme.

Example with 4 tabs (read-only / privateKey / signer / custom) is in example/lib/main.dart:56 – the “Custom” tab uses AlertDialog as above.


Security #

  • Private key / Signer: never log, never commit, store in flutter_secure_storage with biometrics, wipe on logout. Prefer Web3Signer (WalletConnect / Secure Enclave) over embedding keys. EthereumProvider.initialize (lib/ethereum/ethereum_provider.dart:63) validates privateKey if provided; Web3WebView shows error, not WebView, on invalid format. Read-only mode needs no key. Clipboard uses flutter/services without logging (lib/utils/app_utils.dart:5).
  • XSS: all JS interpolation uses jsonEncode (lib/provider/provider_script.dart:13, lib/web3_js_bridge_callback.dart:5, lib/ethereum/ethereum_provider.dart:841).
  • Origin: dialogs display controller.getUrl().host; consider allowlist / phishing check before showConnectWallet.
  • RPC: rpcUrls.first is used; use HTTPS + API key, pin certs via onReceivedServerTrustAuthRequest.
  • Singleton: EthereumProvider is a singleton; multiple Web3WebView share state – avoid mounting two simultaneously, call dispose() (lib/ethereum/ethereum_provider.dart:259) on screen dispose (already done in Web3WebView).

Limitations #

  • Singleton EthereumProvider – not multi-account, not multi-chain concurrent.
  • ethers.min.js (464 KB) bundled at packages/web3_webview/assets/ethers.min.js, injected twice.
  • No eth_subscribe/eth_getLogs polling; unsupported methods throw 4200.
  • LoadingHelper is global ref-counted overlay (lib/utils/loading.dart:3).

Changelog #

See CHANGELOG.md. 1.0.13 adds WalletDialogBuilders for fully custom UI (dialogBuilders), 1.0.12 premium + auto dark, 1.0.11 Web3Signer + read-only.


License & Thanks #

  • License: see LICENSE (BSD).
  • Thanks: PositionExchange/flutter-web3-provider, ethers.js v6 docs.
7
likes
150
points
62
downloads

Documentation

API reference

Publisher

verified publisherdatit309.is-a.dev

Weekly Downloads

web3_webview is a powerful bridge between a DApp running inside a WebView and your Flutter application, enabling secure and seamless two-way communication between them

Repository (GitHub)
View/report issues

License

BSD-3-Clause (license)

Dependencies

crypto, eip55, flutter, flutter_inappwebview, hex, http, intl, modal_bottom_sheet, uuid, web3dart

More

Packages that depend on web3_webview