Flutter Web App Ethereum Checkout & Interaction

Photo by Art Rachen on Unsplash

Flutter Web App Ethereum Checkout & Interaction

Build a flutter web app that interacts with the ethereum network to perform an eth payment

ยท

8 min read

Payment options and checkouts are crucial aspects of the modern world. With crypto gaining acceptability around the world, including it as a payment option will be boost the platform. This article discusses how to use the Ethereum network to checkout using Flutter for Web. We will write a simple smart contract that accepts payments from users wallets through providers such as metamask.

1. Set up

We are going to use hardhat to engineer the smart contract. Here are some simple instructions on how to setup.

open cmd line in the folder where you want to save the project and run this commands:

npm init --yes

npm install --save-dev hardhat

npx hardhat

npm install --save-dev @nomiclabs/hardhat-waffle ethereum-waffle chai @nomiclabs/hardhat-ethers ethers

2. Write the smart contract

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.10;

contract CartCheckout{

    //we declare a variable to hold our contract balance
    uint public salesBalance;

    //we declare an event that emits state everytime there is a payment
    event ethPayment(address indexed from,uint256 indexed ethAmount);

    function checkContractBalance() public view returns(uint256 balance){
        //returns contract balance in the smallest unit wei
        return address(this).balance;
    }

    //this function receives eth from other addresses
    //the payable keyword is necessary to allow the contract 
    //to receive eth
    function cartPayout() public payable {
        //add received eth to our balance variable
        salesBalance += msg.value;
        //emit an event when eth is received
        emit ethPayment(msg.sender, msg.value);
    }
}

3. Deploying the contract

In the scripts folder, create a new file deploy.js and add the following code.

const { ethers } = require("hardhat");
require("dotenv").config({ path: ".env" });

async function main() {

    /*
      A ContractFactory in ethers.js is an abstraction used to deploy new smart contracts,
      so CartCheckout here is a factory for instances of our CartCheckout contract.
      */
    const contract = await ethers.getContractFactory(
        "CartCheckout"
    );

    // deploy the contract
    const deployedContract = await contract.deploy(
    );

    // print the address of the deployed contract
    console.log(
        "Contract Address:",
        deployedContract.address
    );
}

// Call the main function and catch if there is any error
main()
    .then(() => process.exit(0))
    .catch((error) => {
        console.error(error);
        process.exit(1);
    });

We are going to use Alchemy to expose our smart contract to the Ethereum blockchain. We are also going to use the Ethereum Rinkeby test network.

  • create a .env file on the root folder of your project and add the following lines into the file which include simple instructions on how to set up Alchemy.
// Go to https://www.alchemyapi.io, sign up, create
// a new App in its dashboard and select the network as Rinkeby, and replace "Add your alchemy api url here" with its key url
ALCHEMY_API_KEY_URL="Add your alchemy api url here"

// Replace this private key with your RINKEBY account private key
// To export your private key from Metamask, open Metamask and
// go to Account Details > Export Private Key
// Be aware of NEVER putting real Ether into testing accounts
RINKEBY_PRIVATE_KEY="*****************"

Open cmd on the root folder and run this command so as to install dotenv package which enables us to load and process the .env file.

npm install dotenv

Now open the hardhat.config.js file. Replace all the lines in the file with the given ones below.

require("@nomiclabs/hardhat-waffle");
require("dotenv").config({ path: ".env" });

const ALCHEMY_API_KEY_URL = process.env.ALCHEMY_API_KEY_URL;

const RINKEBY_PRIVATE_KEY = process.env.RINKEBY_PRIVATE_KEY;

module.exports = {
  solidity: "0.8.10",
  networks: {
    rinkeby: {
      url: ALCHEMY_API_KEY_URL,
      accounts: [RINKEBY_PRIVATE_KEY],
    },
  },
};

Now we can compile the contract and deploy it.

npx hardhat compile

npx hardhat run scripts/deploy.js --network rinkeby

Take note of the contract address printed on the console that we will use later.

4. Flutter Setup

Create a new flutter project and add the following import in pubspec.yaml

flutter_web3:

Lets dive into the code for our flutter frontend.

Replace the contents of main.dart file with the following.

void main() => runApp(MaterialApp(
      initialRoute: '/',
      routes: {
        '/': (context) => CartApp(),
      },
    ));

class CartApp extends StatefulWidget {
  @override
  _CartAppState createState() => _CartAppState();
}

class _CartAppState extends State<CartApp> {
  List<ProductModel> products = [
    ProductModel("Circular Vase", 100, "vase.jpg"),
    ProductModel("Sun Vase", 200, "vase2.jpg"),
    ProductModel("Clear Vase", 250, "vase3.jpg"),
    ProductModel("Twin Vase", 300, "vase4.jpg"),
    ProductModel("White Vase", 350, "vase1.jpg"),
  ];
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text("Nice Shop"),
      ),
      body: ListView(
        padding: EdgeInsets.symmetric(
          horizontal: MediaQuery.of(context).size.width / 5,
        ),
        scrollDirection: Axis.vertical,
        children: List.generate(products.length, (index) {
          return Container(
            margin: const EdgeInsets.all(20),
            constraints: BoxConstraints(
              maxHeight: MediaQuery.of(context).size.height / 3,
              maxWidth: MediaQuery.of(context).size.width / 3,
            ),
            child: Stack(
              children: [
                Image(
                  fit: BoxFit.fill,
                  height: double.infinity,
                  width: double.infinity,
                  image: AssetImage('assets/images/${products[index].image}'),
                ),
                Positioned(
                  left: 10,
                  right: 10,
                  bottom: 5,
                  child: Row(
                    mainAxisAlignment: MainAxisAlignment.spaceBetween,
                    children: [
                      Column(
                        crossAxisAlignment: CrossAxisAlignment.start,
                        children: [
                          Text(
                            products[index].name,
                            style: const TextStyle(fontSize: 30),
                          ),
                          Text(
                            "\Wei ${products[index].price}",
                            style: const TextStyle(
                                color: Colors.green,
                                fontSize: 20,
                                fontWeight: FontWeight.w500),
                          ),
                        ],
                      ),
                      RaisedButton(
                        onPressed: () {
                          Navigator.of(context).push(MaterialPageRoute(
                              builder: (_) => Checkout(
                                  amount: products[index].price)));
                        },
                        child: const Text(
                          'Buy',
                          style: TextStyle(
                            color: Colors.white,
                          ),
                        ),
                        color: Colors.blueAccent,
                        shape: RoundedRectangleBorder(
                          borderRadius: BorderRadius.circular(10),
                        ),
                      ),
                    ],
                  ),
                ),
              ],
            ),
          );
        }),
      ),
    );
  }
}

Create a new file model_product.dart in the lib folder which will hold our shopping items data object.

Add the following lines into the file.

class ProductModel {
  var name;
  var price;
  var image;

  ProductModel(String name, int price, String image) {
    this.name = name;
    this.price = price;
    this.image = image;
  }
}

Create an assets folder in the flutter project root folder and then another folder named images in it.

Add any image and rename it to vase.jpg

Then create a new file in assets folder named cart_checkout_abi.json.

This file will hold our contract abi details for the web app to interact with.

Head over to the hardhat project, into this file ...\artifacts\contracts\cart_checkout.sol\CartCheckout.json and copy the contents of the abi field into the above new file.

code.png

Add the following lines into pubspec.yaml so as to import the assets into the project

assets:
    - assets/
    - assets/images/

With that done we can now connect our web app with metamask for a checkout.

Create a new file in the lib folder of the flutter project and name it checkout.dart

Copy the following contents into it.

class Checkout extends StatefulWidget {
  final int amount;
  const Checkout({Key? key, required this.amount}) : super(key: key);

  @override
  State<Checkout> createState() => _CheckoutState();
}

class _CheckoutState extends State<Checkout> {
  int currentChainId = -1;
  late Web3Provider web3Client;
  Contract? contract;
  //Rinkkeby chain network id
  static const int chainId = 4;
  static String walletAddress = '';

  @override
  Widget build(BuildContext context) {
    return Scaffold(
        appBar: AppBar(title: const Text('Connect Wallet')),
        body: ListView(
          padding: const EdgeInsets.all(50),
          children: [
            //we need to check whether the user is on the rinkeby network
            //as that is the chain we are using to test
            walletAddress.isNotEmpty && currentChainId != chainId
                ? const Text('Please Change your Wallet account to Rinkeby')
                : const SizedBox(),
            walletAddress.isEmpty
                ? Center(
                    child: ElevatedButton(
                        onPressed: () async {
                          // `Ethereum.isSupported` is the same as `ethereum != null`
                          if (ethereum != null) {
                            try {
                              // Prompt user to connect to the provider, i.e. confirm the connection modal
                              final accs = await ethereum!
                                  .requestAccount(); // Get all accounts in node disposal
                              currentChainId = await ethereum!.getChainId();

                              walletAddress = accs.first;
                              ethereum!.onAccountsChanged((accs) {
                                walletAddress = accs.first;
                                setState(() {});
                              });

                              ethereum!.onChainChanged((chain) {
                                currentChainId = chain;
                                setState(() {});
                              });
                              web3Client = Web3Provider(ethereum);
                              await loadContract();
                              listenTransactionEvents();
                              setState(() {});
                            } on EthereumUserRejected {
                              print('User rejected the modal');
                            } catch (e) {
                              print(e);
                            }
                          }
                        },
                        child: const Text('Connect Wallet')),
                  )
                : const SizedBox(),
            chainId == currentChainId && walletAddress.isNotEmpty
                ? ElevatedButton(
                    onPressed: () {
                      buyGoods();
                    },
                    child: Text('Confirm Checkout of ${widget.amount} Wei'))
                : const SizedBox()
          ],
        ));
  }

  Future<bool> buyGoods() async {
    try {
      //we call the contract function to make payment and pass in the amount
      //we convert the amount to BigInt as that is the equivalent of the data type the contract expects
      //This function call requires gas fees so make sure your rinkeby account has some eth in it
      var sellRes = await contract!.send(
        'cartPayout',
        [],
        TransactionOverride(value: BigInt.from(widget.amount)),
      );

      print('cart payout ${sellRes.hash}');
      var res = await contract!.call('checkContractBalance');
      print('contract balance $res');
      //This prints the previous contract balance as the new transaction has not been finalized yet
      return true;
    } catch (e) {
      print(e);
      var res = await contract!.call('checkContractBalance');
      print('contract balance $res');
      return false;
    }
  }

  void listenTransactionEvents() async {
    //Listen for the payment events coming from this address with the specified amount
    //At this time the transaction can be considered complete and verified
    final filter = contract!.getFilter('ethPayment', [
      await web3Client.getSigner().getAddress(),
      BigNumber.from(widget.amount.toString())
    ]);

    contract!.on(filter, (from, amount, event) {
      //You can perform any UI actions here to show the user that the
      //transaction is complete
      print('transaction event--------- ${Event.fromJS(event).event}');
    });
  }

  Future<void> loadContract() async {
    //loads the ABI json file from assets and converts it into a String
    String abi = await rootBundle.loadString('assets/cart_checkout_abi.json');
    //Get the contract address and paste it here
    String contractAddress = '*****************';
    //Pass in the json String and the abi of the smart contract.
    contract = Contract(contractAddress, abi, web3Client.getSigner());
  }
}

Get some test eth on this link so as to pay for gas fees and perform transactions.

You can run this command on your terminal to run the app

flutter run -d web-server

Open the url printed on the console and open in your chrome browser having a metamask extension.

Watch the chrome console logs for insights on whats happening.

Remember to refresh the page everytime you make changes on the code. Flutter Reload or refresh on the web does not effect the changes.

With that you can perform a simple eth checkout in your flutter web app.

Looking to learn more about web3, head over to learn Web3 dao where I have drawn lots of knowledge to write this smart contract.

Till we meet again fellas ๐Ÿ‘‹๐Ÿ‘‹

ย