This is an enterprise-only feature. Please contact us to enable.
ASWebAuthenticationSession on iOS.
The basic integration is fireblocks_connect.dart. For a fully native wallet list with signing (and optional send), see Flutter headless.
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.View fireblocks_connect.dart (copy-paste ready)
View fireblocks_connect.dart (copy-paste ready)
fireblocks_connect.dart
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();
}
}
View models.dart (copy-paste ready)
View models.dart (copy-paste ready)
models.dart
/// 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);
}
1. Add dependencies
pubspec.yaml
dependencies:
flutter_web_auth_2: 4.0.1
url_launcher: 6.3.1
2. Register your URL scheme
For iOS, add toios/Runner/Info.plist.
Info.plist
<key>CFBundleURLTypes</key>
<array><dict>
<key>CFBundleURLSchemes</key>
<array><string>myapp</string></array>
</dict></array>
<activity> in AndroidManifest.xml.
AndroidManifest.xml
<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
connect_button.dart
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.
Real wallet round-trips require a physical device.