> ## Documentation Index
> Fetch the complete documentation index at: https://www.dynamic.xyz/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Hosted wallet connector on Flutter (basic)

> Present the hosted connect page with flutter_web_auth_2 and read the wallet address on your custom URL scheme.

<Note>
  This is an enterprise-only feature. Please [contact us](https://www.dynamic.xyz/book-a-call) to enable.
</Note>

On Flutter the [hosted connect page](/docs/connections/overview) contract is the same as every other platform, using Chrome Custom Tabs on Android and `ASWebAuthenticationSession` on iOS.

The basic integration is `fireblocks_connect.dart`. For a fully native wallet list with signing (and optional send), see [Flutter headless](/docs/connections/flutter-headless).

<Tip>
  `flutter_web_auth_2` wraps both platform auth-session APIs in one call. It is the Flutter equivalent of iOS's `ASWebAuthenticationSession` and Android's Chrome Custom Tabs.
</Tip>

<AccordionGroup>
  <Accordion title="View fireblocks_connect.dart (copy-paste ready)">
    ````dart fireblocks_connect.dart theme={"system"}
    import 'dart:math';

    import 'package:flutter_web_auth_2/flutter_web_auth_2.dart';

    import 'models.dart';

    /// User cancelled or dismissed the system browser without completing the flow.
    class FireblocksConnectCancelled implements Exception {
      const FireblocksConnectCancelled();
      @override
      String toString() => 'FireblocksConnectCancelled';
    }

    /// Malformed callback URL or nonce mismatch.
    class FireblocksConnectError implements Exception {
      final String message;
      const FireblocksConnectError(this.message);
      @override
      String toString() => 'FireblocksConnectError: $message';
    }

    /// Opens the visible wallet-select flow in a system browser
    /// (Chrome Custom Tabs on Android, ASWebAuthenticationSession on iOS).
    ///
    /// Use this when [FireblocksHeadlessConnect] calls back with
    /// [ConnectFallbackRequired], or as a standalone connect path.
    ///
    /// ```dart
    /// try {
    /// final wallet = await FireblocksConnect.connect(
    /// flowUrl: 'https://connect.dynamicauth.com/',
    /// scheme: 'fbapp',
    /// environmentId: 'b1e3aca9-0646-411a-b4ab-c31ce49935b3',
    /// );
    /// // wallet.address, wallet.chain, …
    /// } on FireblocksConnectCancelled {
    /// // user dismissed
    /// } on FireblocksConnectError catch (e) {
    /// print(e);
    /// }
    /// ```
    abstract final class FireblocksConnect {
      /// Open the hosted connect page and wait for the wallet-callback redirect.
      ///
      /// [flowUrl] — your hosted Fireblocks connect page URL.
      /// [scheme]  — your app's registered URL scheme (must match Info.plist,
      ///             AndroidManifest.xml, and the `flutter_web_auth_2`
      ///             `CallbackActivity` intent-filter).
      /// [environmentId] — optional Dynamic environment ID for the hosted page to
      ///             use instead of its built-in default. Omit (or pass empty) to
      ///             leave the page on its own default.
      static Future<WalletConnection> connect({
        required String flowUrl,
        required String scheme,
        String environmentId = '',
      }) async {
        final nonce = _randomHex(16);
        final base = Uri.parse(flowUrl);
        final uri = base.replace(queryParameters: {
          ...base.queryParameters,
          'redirect_uri': '$scheme://wallet-callback',
          'nonce': nonce,
          'embedded': '1',
          if (environmentId.isNotEmpty) 'environmentId': environmentId,
        });

        final String callbackUrl;
        try {
          callbackUrl = await FlutterWebAuth2.authenticate(
            url: uri.toString(),
            callbackUrlScheme: scheme,
            options: const FlutterWebAuth2Options(preferEphemeral: true),
          );
        } on Exception catch (e) {
          final msg = e.toString().toLowerCase();
          if (msg.contains('cancel') || msg.contains('dismiss') || msg.contains('usercancel')) {
            throw const FireblocksConnectCancelled();
          }
          rethrow;
        }

        return _parseCallback(callbackUrl, expectedNonce: nonce);
      }

      static WalletConnection _parseCallback(String callbackUrl, {required String expectedNonce}) {
        final uri = Uri.parse(callbackUrl);
        if (uri.queryParameters['nonce'] != expectedNonce) {
          throw const FireblocksConnectError('Nonce mismatch — possible replay attack');
        }
        final address = uri.queryParameters['address'] ?? '';
        if (address.isEmpty) {
          throw const FireblocksConnectError('No address in callback URL');
        }
        return WalletConnection(
          address: address,
          chain: uri.queryParameters['chain'] ?? '',
          walletName: uri.queryParameters['walletName'] ?? '',
          walletImage: uri.queryParameters['walletImage'] ?? '',
          connectedHeadlessly: false,
        );
      }

      static String _randomHex(int bytes) {
        final rng = Random.secure();
        return List.generate(bytes, (_) => rng.nextInt(256))
            .map((b) => b.toRadixString(16).padLeft(2, '0'))
            .join();
      }
    }
    ````
  </Accordion>
</AccordionGroup>

<AccordionGroup>
  <Accordion title="View models.dart (copy-paste ready)">
    ```dart models.dart theme={"system"}
    /// A wallet entry in the native list, delivered live by the engine.
    /// [mode] is `"headless"` (silent connection) or `"fallback"` (visible flow needed).
    class HeadlessWallet {
      final String key;
      final String name;
      final String icon;
      final List<String> chains;
      final String mode;
      final bool featured;

      const HeadlessWallet({
        required this.key,
        required this.name,
        required this.icon,
        required this.chains,
        required this.mode,
        required this.featured,
      });

      factory HeadlessWallet.fromJson(Map<String, dynamic> json) => HeadlessWallet(
            key: json['key'] as String? ?? '',
            name: json['name'] as String? ?? '',
            icon: json['icon'] as String? ?? '',
            chains: (json['chains'] as List<dynamic>?)?.cast<String>() ?? const [],
            mode: json['mode'] as String? ?? 'fallback',
            featured: json['featured'] as bool? ?? false,
          );

      bool get isMultiChain => chains.length > 1;
      bool get isHeadless => mode == 'headless';
    }

    /// A successfully connected wallet account.
    class WalletConnection {
      final String address;
      final String chain;
      final String walletName;
      final String walletImage;

      /// `true` for wallets connected through the hidden WebView engine;
      /// `false` for the visible ASWebAuth / Chrome Custom Tabs fallback flow.
      final bool connectedHeadlessly;

      /// The [HeadlessWallet.key] that produced this connection, e.g. `"metamask"`.
      ///
      /// NOT part of the engine's wire protocol (the `connected` bridge message
      /// carries no wallet key) — set locally by the caller that made the
      /// `connect()` call, so the UI can offer a one-tap "Reconnect" to the same
      /// wallet on a later app launch. `null` when unknown.
      final String? walletKey;

      const WalletConnection({
        required this.address,
        required this.chain,
        required this.walletName,
        required this.walletImage,
        this.connectedHeadlessly = true,
        this.walletKey,
      });

      WalletConnection copyWith({String? walletKey}) => WalletConnection(
            address: address,
            chain: chain,
            walletName: walletName,
            walletImage: walletImage,
            connectedHeadlessly: connectedHeadlessly,
            walletKey: walletKey ?? this.walletKey,
          );
    }

    /// Describes a failed sign operation.
    class SignError {
      final String code;
      final String message;

      const SignError({required this.code, required this.message});

      @override
      String toString() => '[$code] $message';
    }

    // ── Connect result ───────────────────────────────────────────────────────────

    sealed class ConnectResult {
      const ConnectResult();
    }

    final class ConnectSuccess extends ConnectResult {
      final WalletConnection wallet;
      const ConnectSuccess(this.wallet);
    }

    /// The wallet can't go headless; caller should open [FireblocksConnect.connect].
    final class ConnectFallbackRequired extends ConnectResult {
      final String reason;
      const ConnectFallbackRequired(this.reason);
    }

    final class ConnectFailure extends ConnectResult {
      final String code;
      final String message;
      const ConnectFailure({required this.code, required this.message});
    }

    // ── Sign result ──────────────────────────────────────────────────────────────

    sealed class SignResult {
      const SignResult();
    }

    final class SignSuccess extends SignResult {
      /// Signed message: hex signature string.
      /// Signed tx (EVM): RLP-encoded hex. Signed tx (Solana): base64.
      final String value;
      const SignSuccess(this.value);
    }

    final class SignFailure extends SignResult {
      final SignError error;
      const SignFailure(this.error);
    }

    // ── Send result ──────────────────────────────────────────────────────────────

    /// Result of [FireblocksHeadlessConnect.sendTransaction] — EVM only. Unlike
    /// [SignResult] (sign-only, nothing broadcast), this wallet call signs AND
    /// broadcasts the transaction; a [SendSuccess] means it's already on-chain.
    sealed class SendResult {
      const SendResult();
    }

    final class SendSuccess extends SendResult {
      /// The on-chain transaction hash — already submitted, not a raw signed tx.
      final String txHash;
      const SendSuccess(this.txHash);
    }

    final class SendFailure extends SendResult {
      final SignError error;
      const SendFailure(this.error);
    }
    ```
  </Accordion>
</AccordionGroup>

## 1. Add dependencies

```yaml pubspec.yaml theme={"system"}
dependencies:
  flutter_web_auth_2: 4.0.1
  url_launcher: 6.3.1
```

## 2. Register your URL scheme

For iOS, add to `ios/Runner/Info.plist`.

```xml Info.plist theme={"system"}
<key>CFBundleURLTypes</key>
<array><dict>
  <key>CFBundleURLSchemes</key>
  <array><string>myapp</string></array>
</dict></array>
```

For Android, add inside your `<activity>` in `AndroidManifest.xml`.

```xml AndroidManifest.xml theme={"system"}
<intent-filter android:autoVerify="false">
  <action android:name="android.intent.action.VIEW" />
  <category android:name="android.intent.category.DEFAULT" />
  <category android:name="android.intent.category.BROWSABLE" />
  <data android:scheme="myapp" android:host="wallet-callback" />
</intent-filter>
```

## 3. Connect and use the result

```dart connect_button.dart theme={"system"}
import 'package:flutter_app/src/fireblocks_connect.dart';

try {
  final wallet = await FireblocksConnect.connect(
    flowUrl: 'https://connect.dynamicauth.com/',
    scheme: 'myapp',
    environmentId: 'b1e3aca9-0646-411a-b4ab-c31ce49935b3',
  );
  // wallet.address, wallet.chain,
  // wallet.walletName, wallet.walletImage
} on FireblocksConnectCancelled {
  // user dismissed
} on FireblocksConnectError catch (e) {
  // e.message: nonce mismatch or malformed callback
}
```

`walletImage` is usually an SVG-sprite URL. Render it in a `WebViewWidget` rather than `Image.network`.

<Note>
  Real wallet round-trips require a physical device.
</Note>
