Integrating MetaMask with Your Flutter dApp: A Step-by-Step Guide

Comprehensive Guide to Add MetaMask Support in Flutter dApp

In the world of Web3 development, creating decentralized applications (dApps) that interact with blockchain networks is becoming increasingly popular. One crucial aspect of building dApps is integrating wallet functionality to allow users to interact with smart contracts. In this blog post, we'll explore how to integrate MetaMask, one of the most popular Ethereum wallets, into a Flutter dApp.

Why MetaMask?

MetaMask is a browser extension and mobile app that serves as a crypto wallet and a gateway to blockchain apps. It allows users to store and manage account keys, broadcast transactions, send and receive Ethereum-based cryptocurrencies and tokens, and securely connect to decentralized applications. By integrating MetaMask into your Flutter dApp, you're providing users with a familiar and secure way to interact with your application's blockchain features.

Our Demo: RoomFlow

For this tutorial, we'll be using a simplified version of a dApp called RoomFlow. RoomFlow is a decentralized platform for booking and managing spaces (like rooms or apartments) using smart contracts. We'll focus on the SpaceServices class, which handles the interaction between our Flutter app and the Ethereum blockchain.

Prerequisites

Before we dive in, make sure you have the following set up:

  1. Flutter development environment

  2. Basic understanding of Dart and Flutter

  3. Familiarity with Ethereum and smart contracts

  4. MetaMask browser extension or mobile app installed

Step 1: Setting Up Dependencies

First, we need to add the necessary dependencies to our pubspec.yaml file:

dependencies:
  flutter:
    sdk: flutter
  web3dart: ^2.3.5
  http: ^0.13.4
  web_socket_channel: ^2.2.0
  web3modal_flutter: ^2.0.0

Run flutter pub get to install these dependencies.

Step 2: Creating the SpaceServices Class

Now, let's create our SpaceServices class, which will handle the MetaMask integration and blockchain interactions:

import 'package:flutter/material.dart';
import 'package:web3dart/web3dart.dart';
import 'package:http/http.dart' as http;
import 'package:web_socket_channel/io.dart';
import 'package:web3modal_flutter/web3modal_flutter.dart';

class SpaceServices extends ChangeNotifier {
  late Web3Client web3client;
  late W3MService w3mService;
  EthereumAddress? _userAddress;

  final String _rpcUrl = 'http://localhost:7545'; // Replace with your RPC URL
  final String _wsUrl = 'ws://localhost:7545'; // Replace with your WebSocket URL
  final int chainId = 1337; // Replace with your chain ID

  bool get isConnected => _userAddress != null;

  SpaceServices() {
    _init();
  }

  Future<void> _init() async {
    w3mService = W3MService(
      projectId: 'YOUR_PROJECT_ID', // Replace with your WalletConnect project ID
      metadata: const PairingMetadata(
        name: 'RoomFlow',
        description: 'Decentralized Space Booking',
        url: 'https://roomflow.example.com',
        icons: ['https://roomflow.example.com/icon.png'],
      ),
    );

    try {
      await w3mService.init();
    } catch (e) {
      print("Error initializing W3MService: $e");
    }

    web3client = Web3Client(_rpcUrl, http.Client(), socketConnector: () {
      return IOWebSocketChannel.connect(_wsUrl).cast<String>();
    });

    // Try to connect to MetaMask
    await connectToMetaMask();
  }

  Future<void> connectToMetaMask() async {
    try {
      await w3mService.openModal();
      final session = w3mService.session;
      if (session != null) {
        _userAddress = EthereumAddress.fromHex(session.address);
        notifyListeners();
      }
    } catch (e) {
      print("Error connecting to MetaMask: $e");
    }
  }

  Future<void> disconnectWallet() async {
    await w3mService.disconnect();
    _userAddress = null;
    notifyListeners();
  }

  // Example method: Booking a space
  Future<void> bookSpace(BigInt spaceId, BigInt startDate, BigInt endDate, BigInt price) async {
    if (_userAddress == null) {
      throw Exception("Wallet not connected");
    }

    // This is a simplified example. In a real app, you'd interact with your smart contract here.
    final transaction = Transaction(
      to: EthereumAddress.fromHex('0x1234...'), // Your contract address
      from: _userAddress,
      value: EtherAmount.inWei(price),
      data: Uint8List.fromList('bookSpace(uint256,uint256,uint256)'.codeUnits),
    );

    try {
      final signedTransaction = await w3mService.signTransaction(transaction);
      final response = await web3client.sendRawTransaction(signedTransaction);
      print("Transaction sent: $response");
    } catch (e) {
      print("Error booking space: $e");
    }
  }
}

This class sets up the Web3 client, manages the MetaMask connection, and provides methods for connecting, disconnecting, and interacting with the blockchain.

Step 3: Using SpaceServices in Your Flutter App

Now that we have our SpaceServices class, let's see how we can use it in a Flutter widget:

import 'package:flutter/material.dart';
import 'package:provider/provider.dart';

class HomePage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    final spaceServices = Provider.of<SpaceServices>(context);

    return Scaffold(
      appBar: AppBar(title: Text('RoomFlow')),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            if (spaceServices.isConnected)
              Text('Connected: ${spaceServices._userAddress}')
            else
              Text('Not connected'),
            SizedBox(height: 20),
            ElevatedButton(
              onPressed: spaceServices.isConnected
                ? spaceServices.disconnectWallet
                : spaceServices.connectToMetaMask,
              child: Text(spaceServices.isConnected ? 'Disconnect' : 'Connect to MetaMask'),
            ),
            SizedBox(height: 20),
            ElevatedButton(
              onPressed: spaceServices.isConnected
                ? () => spaceServices.bookSpace(
                    BigInt.from(1),
                    BigInt.from(DateTime.now().millisecondsSinceEpoch),
                    BigInt.from(DateTime.now().add(Duration(days: 1)).millisecondsSinceEpoch),
                    BigInt.from(1000000000000000000) // 1 ETH in Wei
                  )
                : null,
              child: Text('Book Space'),
            ),
          ],
        ),
      ),
    );
  }
}

This widget displays the connection status, provides buttons to connect/disconnect the wallet, and shows a "Book Space" button that's only enabled when connected to MetaMask.

Step 4: Setting Up Your Main App

Finally, let's set up the main app to use SpaceServices:

import 'package:flutter/material.dart';
import 'package:provider/provider.dart';

void main() {
  runApp(
    ChangeNotifierProvider(
      create: (context) => SpaceServices(),
      child: MyApp(),
    ),
  );
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'RoomFlow',
      home: HomePage(),
    );
  }
}

Conclusion

In this tutorial, we've walked through the process of integrating MetaMask into a Flutter dApp. We created a SpaceServices class that manages the Web3 client and MetaMask connection, and we've seen how to use this service in a Flutter widget to connect a wallet and interact with a smart contract.

Remember, this is a simplified example. In a real-world application, you'd need to handle more edge cases, implement proper error handling, and likely interact with multiple smart contract functions. You'd also want to add more features like displaying transaction history, showing token balances, and handling different networks.

By integrating MetaMask, you're providing your users with a secure and familiar way to interact with your dApp. This can greatly enhance the user experience and make your application more accessible to the growing Web3 community.

Happy coding, and welcome to the world of decentralized applications!