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
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.
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 ๐๐