Introduction
In this tutorial, we’ll walk through the process of building a decentralized application (DApp) using React Native and Celo Composer. We’ll create a simple DApp that allows users to send and receive tokens on the Celo blockchain. We will also perform contract interactions writing and reading from the blockchain.
We will be using web3Modal to connect our wallet and sign transactions.
By the end of this tutorial, you’ll have a working React Native DApp that you can use to interact with the Celo blockchain and transfer tokens. To easily follow along this tutorial the full code can be found here.
This a preview of what we will be building in this tutorial
Prerequisites
To successfully follow along in this tutorial you need basic knowledge of:
- HTML, CSS, React Native and Expo
- Blockchain, solidity and hardhat
- Celo Valora / Alfajores account
Requirements
To build this DApp we will need the following tools:
- Vscode - But you can use any code editor of your choice
- Hardhat - used to deploy the smart contract
- Valora App - required to connect to the dApp and sign transactions
- Node- an open-source, cross-platform JavaScript runtime environment
- Celo Composer- starter project with all code needed to build, deploy, and upgrade a dapps on Celo.
- React Native Web3Modal- used for connecting our react native dapp to a web3 wallet on mobile
- React Native TailwindCSS - for styling our component
Let’s Get Started
To get started we will be scaffolding our project with Celo Composer. First let’s have some understanding of what Celo composer is about.
What is Celo Composer
Celo-composer is a starter project with all code needed to build, deploy, and upgrade dapps on Celo.
Step 1: Setup the Project
First, let’s set up our project. Create a new directory and run the following commands and follow the steps
Select React Native with expo option and then enter your project name. For details on the steps checkout the Celo Composer github readme page.
Fig 1-0: Celo Composer Setup
Once you have successfully completed the steps do npm install
or yarn
to install all required dependencies. Once that is done you are ready to start building.
Now open your newly created project. You will see a packages folder inside the package folder you will see hardhat and react-app folder.
For security reasons in order not to expose your private keys to the public create a new file named .env
in the root of the hardhat folder add this line of code:
PRIVATE KEY = <YOUR PRIVATE KEY>
Set up a Celo Account
To get started building a simple Greeters Dapp you will need to fund your account, We need to set up a Celo Account. You can use the Celo Mobile Valora Wallet app, which is available on both iOS and Android devices.
Once you have set up your account go to the Celo faucet to get some testnet tokens. You will need to fund your account to deploy the contract and perform other smart contract interactions.
Step 2: Write the Smart Contract
We will be making use of the Greeter.sol
smart contract generated for us after creating our project with Celo Composer. Inside the packages directory navigate to the hardhat directory and open the contract directory, you will see some sample contracts. For the purpose of this tutorial we will be making use of the Greeter.sol
contract. The contract should look like this
//SPDX-License-Identifier: MIT
pragma solidity ^0.8.17;
// remember to remove unnecessary imports and its use when deploying your smart contract
import "hardhat/console.sol";
contract Greeter {
event newGreeting(string greeting, address sender);
string private greeting;
constructor(string memory _greet) {
console.log("Deploying a Greeter with greeting:", _greet);
greeting = _greet;
}
function greet() external view returns (string memory) {
return greeting;
}
function setGreeting(string calldata _greeting) external {
console.log("Changing greeting from '%s' to '%s'", greeting, _greeting);
greeting = _greeting;
emit newGreeting(_greeting, msg.sender);
}
}
Let’s take a closer look at the above code;
- The contract is named
Greeter
and is defined with thecontract
keyword. - The
event newGreeting
is declared to emit an event whenever the greeting is changed. It includes the new greeting string and the address of the sender. - The
string private greeting
is a private state variable that holds the current greeting message. - The constructor function is called when the contract is deployed. It takes a string parameter
_greet
to initialize the greeting message. Theconsole.log
statement is used to log a message with the provided greeting. - The
greet
function is an external view function that allows users to retrieve the current greeting. It returns a string. - The
setGreeting
function is an external function that allows users to change the greeting. It takes a string parameter_greeting
and updates thegreeting
state variable. It also emits thenewGreeting
event with the new greeting and the sender’s address.
Remember to remove the import "hardhat/console.sol";
statement and the console.log
statements if you’re deploying the contract to a production environment. These statements are for debugging purposes and should not be included in the final deployment.
NOTE
This code is a basic example and does not include error handling or access control mechanisms. In a real-world scenario, you would need to consider implementing additional security measures and validations.
Step 3: Deploy the Contract
Inside the hardhat directory navigate to the scripts directory and open the sample-scripts.js file. You will see the deployment code. The code should look like this
// We require the Hardhat Runtime Environment explicitly here. This is optional
// but useful for running the script in a standalone fashion through `node <script>`.
//
// When running the script with `npx hardhat run <script>` you'll find the Hardhat
// Runtime Environment's members available in the global scope.
const hre = require("hardhat");
async function main() {
// Hardhat always runs the compile task when running scripts with its command
// line interface.
//
// If this script is run directly using `node` you may want to call compile
// manually to make sure everything is compiled
// await hre.run('compile');
// We get the contract to deploy
const Greeter = await hre.ethers.getContractFactory("Greeter");
const greeter = await Greeter.deploy("Hello, Hardhat!");
await greeter.deployed();
console.log("Greeter deployed to:", greeter.address);
}
// We recommend this pattern to be able to use async/await everywhere
// and properly handle errors.
main()
.then(() => process.exit(0))
.catch((error) => {
console.error(error);
process.exit(1);
});
To deploy the above code from your terminal or command prompt run npx hardhat run scripts/sample-script.js
. This will give you the deployment address and the generated ABI. The contract address and ABI will be used later in our react native app during contract interactions. You can find the generated ABI for the Greeter contract in the artifact directory. artifacts/contracts/Greeter.sol/greeter.json
. Copy the json file and create a new file inside the react-native-app directory name the file greeter.json and paste the abi code there. Also take note of the deployed contract address.
Step 4: React Native Frontend Interactions
To interact with the above deployed contract and to perform all other frontend related activities, navigate to the react-native-app directory. There are a bunch of files already created for us. but for this tutorial we will be creating one additional screens and making some few changes to the existing code to suite our purpose.
NOTE
Before diving deeper into the frontend code there are some things to note and some challenges you might likely encounter and how to fix that.
As at the time of this writing, if you are using the Celo composer to create your react app you might encounter issues connecting your dapp with wallet connect as there is currently an upgrade to V2.
To avoid connection issues there are some couple of changes that needs to be made. First you will need to install web3Modal and replace wherever you see useWalletConnect with useWeb3Modal.
To install web3modal from your terminal if using npm run npm install @web3modal/react-native
or yarn run yarn add @web3modal/react-native
Additionally add these extra packages to help with async storage, polyfills, modals and SVG’s.
yarn add @react-native-async-storage/async-storage react-native-get-random-values react-native-modal react-native-svg
For more detail setup for web3modal you can follow the guide here
Incase you encounter bigInt issue. you will need to install it. npm i big-integer
Also navigate to the global.ts file inside the react-native-app directory and add this
if (typeof BigInt === "undefined") global.BigInt = require("big-integer");
For our react native app styling we will be making use of react native tailwind for that you will install this package
npm install twrnc
Check this for detail guide on how to use the library.
Enough talk now let’s get down to the code. For this section ensure you are in the react-native-app directory as we will be visiting some of the files and making some changes.
Greeter.tsx
Navigate to Screens/Greeter.tsx
file. After the changes we made your code should look like this;
import { useState, useEffect, useContext } from "react";
import { Text, View, } from "../components/Themed";
import { TextInput, Button, TouchableOpacity, ScrollView } from "react-native";
import * as WebBrowser from "expo-web-browser";
import Web3 from "web3";
import { ThemeContext } from "../context/ThemeProvider";
import { useWeb3Modal } from "@web3modal/react-native";
import { ethers } from 'ethers';
import { useMemo } from 'react';
import greeterABI from "../greeter.json"
import tw from "twrnc"
import AlertMessage from "../components/Alert";
import { RefreshControl } from "react-native";
const web3 = new Web3("https://alfajores-forno.celo-testnet.org");
export default function Greeter() {
const { styles } = useContext(ThemeContext);
const { provider, address } = useWeb3Modal()
const web3Provider = useMemo(
() => (provider ? new ethers.providers.Web3Provider(provider) : undefined),
[provider]
);
const contractAddress = "0x421d8e7f1717A6B3B104865Da28bC3620fa3A829";
// const { contractData } = props;
const [greeterValue, setGreeterValue] = useState("");
const [greetingInput, setGreetingInput] = useState("");
const [contractLink, setContractLink] = useState("");
const [settingGreeting, setSettingGreeting] = useState(false);
const [loadGreeting, setLoadGreeting] = useState(false);
const [signer, setSigner] = useState<ethers.providers.JsonRpcSigner | undefined>(undefined);
const [refreshing, setFreshing] = useState(false)
const contract = new web3.eth.Contract(greeterABI.abi, contractAddress)
useEffect(() => {
setContractLink(
`https://alfajores-blockscout.celo-testnet.org/address/${contractAddress}`
);
}, []);
const sendGreeting = async () => {
setSettingGreeting(true);
const sign = web3Provider?.getSigner();
setSigner(sign);
if (!greetingInput) {
return AlertMessage("Greeting", "Input field required")
} else {
try {
const txResponse = signer && await signer.sendTransaction({
to: contractAddress, // Replace with the contract address
data: contract.methods.setGreeting(greetingInput).encodeABI(),
});
console.log('Transaction sent:', txResponse);
setGreetingInput("")
getGreeting()
} catch (e) {
console.error('Failed to send transaction:', e);
} finally {
setSettingGreeting(false);
}
}
};
const getGreeting = async () => {
setFreshing(true)
setLoadGreeting(true);
try {
const result = (await contract?.methods.greet().call()) as string;
setGreeterValue(result);
setLoadGreeting(false);
setFreshing(false)
} catch (e) {
console.log(e);
} finally {
setLoadGreeting(false);
setFreshing(false)
}
};
function handlePress() {
WebBrowser.openBrowserAsync(contractLink);
}
useEffect(() => {
getGreeting()
},[greeterValue])
return (
<View style={tw `flex-1 justify-center pt-36 px-8`}>
<View style={styles.innerContainer}>
<Text style={styles.title}>Greeter Contract</Text>
<TouchableOpacity
style={styles.externalLink}
onPress={() => handlePress()}
>
<Text style={styles.externalLink}>
{`${contractAddress.substr(
0,
5
)}...${contractAddress.substr(-5)}`}
</Text>
</TouchableOpacity>
</View>
<View style={styles.separator}></View>
<View style={styles.innerContainer}>
<Text>Write Contract</Text>
<TextInput
value={greetingInput}
onChangeText={(newValue) => setGreetingInput(newValue)}
style={styles.textInput}
/>
<View style={tw `border rounded` }>
<Button title="Send Greeting" onPress={() => sendGreeting()}/>
</View>
</View>
<View style={styles.separator}></View>
<ScrollView
refreshControl={
<RefreshControl refreshing={refreshing} onRefresh={getGreeting} />
}>
<View style={styles.innerContainer}>
<Text style={tw `text-lg font-bold`}>Read Contract</Text>
{loadGreeting ? <Text style={ tw `text-lg`}>Fetching Greeting...</Text> :
greeterValue &&
<View>
<Text style={{ marginVertical: 10 }}>
Greeter Contract Value: {greeterValue}
</Text>
</View>
}
</View>
</ScrollView>
</View>
);
}
The above code contains a TextInput field to enter greetings and also a button that will call the greetings function, and a useEffect to call the getGreeting function on initial render. Also a text field to display the updated state of greeting value.
The sendGreeting
function will write to the blockchain. After contract initialization.
getGreeting
function will get the greeting value from the blockchain.
handlepress
function will open the web browser to view the transactions detail on the celo alfajores block explorer.
TransferScreen.tsx
Create a new file inside the Screens directory called TransferScreen Screens/TransferScreen.tsx
file. Copy and paste this code inside. Your code should look like this;
import React, { useState, useMemo, useEffect } from 'react'
import { TextInput, Text, Button, View } from 'react-native'
import { useWeb3Modal } from '@web3modal/react-native';
import { ethers, BigNumber } from 'ethers';
import tw from "twrnc"
import AlertMessage from '../components/Alert';
export default function TransferScreen() {
const { address, provider } = useWeb3Modal()
const [signer, setSigner] = useState<ethers.providers.JsonRpcSigner | undefined>(undefined)
const [receiver, setReceiver] = useState<string>("")
const [amount, setAmount] = useState<string>("")
const [balance, setBalance] = useState<string>("0")
const [loading, setLoading] = useState<boolean>(false)
const web3Provider = useMemo(
() => (provider ? new ethers.providers.Web3Provider(provider) : undefined),
[provider]
);
const sendTransaction = async () => {
const sign = web3Provider?.getSigner()
setSigner(sign)
try {
if (!receiver || !amount) {
return AlertMessage("Send", "Input field required")
} else {
const txResponse = signer && await signer.sendTransaction({
from: address,
to: receiver,
value: ethers.utils.parseEther(amount)
});
console.log('Transaction sent:', txResponse);
}
} catch (error) {
console.error('Failed to send transaction:', error);
}
};
const accountBalance = async () => {
setLoading(true)
const response = await web3Provider?.getBalance(address as string)
const convertHexValue = parseInt(response, 16)
setBalance(ethers.utils.formatEther(convertHexValue))
console.log(balance);
setLoading(false)
}
useEffect(() => {
accountBalance()
}, [])
return (
<View style={tw`flex-1 align-item-center justify-center mx-8`}>
<Text style={tw`text-center text-lg my-4`}>Transfer Fund</Text>
{loading ? <Text>Fetching balance...</Text> : <Text>{ `Account Balance ${balance}`}</Text> }
<TextInput style={tw`border p-2 rounded`} placeholder='enter receiver addresss' value={receiver} onChangeText={(newValue) => setReceiver(newValue)}/>
<TextInput style={tw`border p-2 rounded my-2`} placeholder='enter amount' value={amount} onChangeText={(newValue) => setAmount(newValue)} />
<View style={tw`border-0 rounded`}>
<Button title='Transfer Fund' onPress={sendTransaction}/>
</View>
</View>
)
}
The provided code is a React Native component for a transfer screen in a mobile app. It allows users to send funds (Ether) to another address using the Web3Modal library. Here’s a breakdown of the code:
- The component imports necessary dependencies, including React Native components, the Web3Modal library, ethers.js, and custom components.
- The component defines state variables using the
useState
hook to manage the receiver address, amount, balance, signer, and loading state. - The
useWeb3Modal
hook is used to access the connected address and provider from the Web3Modal context. - The
web3Provider
variable is memoized using theuseMemo
hook to create an instance ofWeb3Provider
. - The
sendTransaction
function is called when the user clicks the “Transfer Fund” button. It retrieves the signer and sends a transaction using the provided receiver address and amount. - The
accountBalance
function fetches the account balance using thegetBalance
method from the Web3Provider.
Navigation Tab
We will be making some changes to the bottom navigation tab. To do that cd out of the Screens directory and navigate to the navigation directory. Open index.tsx and add this two Nvaigation at the bottom
<BottomTab.Screen name="Greeter" component={Greeter} />
<BottomTab.Screen name="Send" component={Transfer} />
After the above changes, the full code inside navigation/index.tsx
should look like this
/**
* If you are not familiar with React Navigation, refer to the "Fundamentals" guide:
* https://reactnavigation.org/docs/getting-started
*
*/
import { SafeAreaProvider } from "react-native-safe-area-context";
import { createBottomTabNavigator } from "@react-navigation/bottom-tabs";
import {
NavigationContainer,
DefaultTheme,
DarkTheme,
} from "@react-navigation/native";
import { createNativeStackNavigator } from "@react-navigation/native-stack";
import * as React from "react";
import { ColorSchemeName, Button } from "react-native";
import Colors from "../constants/Colors";
import useColorScheme from "../hooks/useColorScheme";
import ModalScreen from "../screens/ModalScreen";
import NotFoundScreen from "../screens/NotFoundScreen";
import { RootStackParamList, RootTabParamList } from "../types";
import LinkingConfiguration from "./LinkingConfiguration";
import LoginScreen from "../screens/LoginScreen";
// import deployedContracts from "@celo-composer/hardhat/deployments/hardhat_contracts.json";
import Account from "../screens/Account";
import { useWeb3Modal } from "@web3modal/react-native";
import Docs from "../screens/Docs";
import Greeter from "../screens/Greeter"
import Transfer from "../screens/TransferScreen"
export default function Navigation({
colorScheme,
}: {
colorScheme: ColorSchemeName;
}) {
return (
<NavigationContainer
linking={LinkingConfiguration}
theme={colorScheme === "dark" ? DarkTheme : DefaultTheme}
>
<RootNavigator />
</NavigationContainer>
);
}
/**
* A root stack navigator is often used for displaying modals on top of all other content.
* https://reactnavigation.org/docs/modal
*/
const Stack = createNativeStackNavigator<RootStackParamList>();
function RootNavigator() {
const { isConnected } = useWeb3Modal();
return (
<Stack.Navigator>
{isConnected ? (
<Stack.Screen
name="Root"
// the Root path renders the component mentioned below.
component={BottomTabNavigator}
options={{ headerShown: false }}
/>
) : (
<Stack.Screen
name="Root"
component={LoginScreen}
options={{ headerShown: false }}
/>
)}
<Stack.Screen
name="NotFound"
component={NotFoundScreen}
options={{ title: "Oops!" }}
/>
<Stack.Group screenOptions={{ presentation: "modal" }}>
<Stack.Screen name="Modal" component={ModalScreen} />
</Stack.Group>
</Stack.Navigator>
);
}
/**
* A bottom tab navigator displays tab buttons on the bottom of the display to switch screens.
* https://reactnavigation.org/docs/bottom-tab-navigator
*/
const BottomTab = createBottomTabNavigator<RootTabParamList>();
function BottomTabNavigator() {
const theme = useColorScheme();
// const contracts = deployedContracts["44787"]?.["alfajores"]?.contracts;
return (
<SafeAreaProvider>
<BottomTab.Navigator
// first screen visible after login
initialRouteName="Docs"
screenOptions={{
headerShown: false,
tabBarActiveTintColor: Colors["brand"].light.text,
tabBarActiveBackgroundColor:
Colors["brand"][theme].background,
tabBarLabelPosition: "beside-icon",
tabBarIconStyle: { display: "none" },
tabBarLabelStyle: { textAlign: "center" },
}}
>
{/* <BottomTab.Screen
name="Greeter"
children={(props) => (
<Greeter contractData={contracts.Greeter} {...props} />
)}
options={() => ({
title: "Greeter Contract",
headerShown: false,
// render icons if any
tabBarIcon: ({
focused: boolean,
color: string,
size: number,
}) => {
return <></>;
},
tabBarLabelPosition: "beside-icon",
})}
/>
<BottomTab.Screen
name="Storage"
children={(props) => (
<Storage contractData={contracts.Storage} {...props} />
)}
options={{
title: "Storage Contract",
headerShown: false,
tabBarIcon: ({
focused: boolean,
color: string,
size: number,
}) => {
return <></>;
},
tabBarLabelPosition: "beside-icon",
}}
/> */}
<BottomTab.Screen name="Docs" component={Docs} />
<BottomTab.Screen
name="Account"
component={Account}
options={() => ({
title: "Account",
headerShown: false,
tabBarIcon: ({
focused: boolean,
color: string,
size: number,
}) => {
return <></>;
},
tabBarLabelPosition: "beside-icon",
})}
/>
<BottomTab.Screen name="Greeter" component={Greeter} />
<BottomTab.Screen name="Send" component={Transfer} />
</BottomTab.Navigator>
</SafeAreaProvider>
);
}
The provided code is a navigation configuration using the React Navigation library. It sets up a stack navigator and a bottom tab navigator to handle navigation between screens.
Here’s a breakdown of the code:
- The
Navigation
component is the entry point and wraps the entire app with theNavigationContainer
component from React Navigation. It provides the navigation context and sets the theme based on the color scheme. - The
RootNavigator
component is a stack navigator responsible for handling navigation between screens. It conditionally renders theBottomTabNavigator
if the user is connected (based on theisConnected
flag from theuseWeb3Modal
hook), otherwise it renders theLoginScreen
. It also includes aNotFoundScreen
for handling unknown routes and a modal screen. - The
BottomTabNavigator
component is a bottom tab navigator that displays tab buttons at the bottom of the screen for switching between screens. It includes several screens:Docs
,Account
,Greeter
, andSend
. - Each screen in the
BottomTabNavigator
is defined using theBottomTab.Screen
component. Screens likeGreeter
andSend
have corresponding components (Greeter
andTransfer
) that are rendered when the respective tab is active. These components can be customized with additional props and options.
NOTE
At the root of the react-native-app directory create a file called expo-crypto-shim.js
and add this file.
// https://github.com/expo/expo/issues/17270#issuecomment-1445149952
// Polyfill for expo-crypto until issue with react-native-get-random-values is solved
// Apply only with Expo SDK >= 48
import { getRandomValues as expoCryptoGetRandomValues } from 'expo-crypto';
class Crypto {
getRandomValues = expoCryptoGetRandomValues;
}
// eslint-disable-next-line no-undef
const webCrypto = typeof crypto !== 'undefined' ? crypto : new Crypto();
(() => {
if (typeof crypto === 'undefined') {
Object.defineProperty(window, 'crypto', {
configurable: true,
enumerable: true,
get: () => webCrypto,
});
}
})();
Still at the root of the react-native-app directory your global.ts
file should look like this
// https://github.com/expo/expo/issues/17270#issuecomment-1445149952
// Polyfill for expo-crypto until issue with react-native-get-random-values is solved
// Apply only with Expo SDK >= 48
import { getRandomValues as expoCryptoGetRandomValues } from 'expo-crypto';
class Crypto {
getRandomValues = expoCryptoGetRandomValues;
}
// eslint-disable-next-line no-undef
const webCrypto = typeof crypto !== 'undefined' ? crypto : new Crypto();
(() => {
if (typeof crypto === 'undefined') {
Object.defineProperty(window, 'crypto', {
configurable: true,
enumerable: true,
get: () => webCrypto,
});
}
})();
App.tsx
After adding the Web3Modal component and imports. The full code of your app.tsx file should look like this
import React from "react";
// // Only for Expo SDK 48+
import './expo-crypto-shim.js'
import { StatusBar } from "expo-status-bar";
import { SafeAreaProvider } from "react-native-safe-area-context";
import { useEffect } from "react";
import { LogBox } from "react-native";
import useCachedResources from "./hooks/useCachedResources";
import useColorScheme from "./hooks/useColorScheme";
import Navigation from "./navigation";
import { Web3Modal } from "@web3modal/react-native";
import { providerMetadata, sessionParams } from "./constants/Config.js";
// import { REACT_APP_ENV_PROJECT_ID } from "@env";
import { ThemeProvider } from "./context/ThemeProvider";
export default function App() {
const isLoadingComplete = useCachedResources();
const colorScheme = useColorScheme();
// avoid warnings showing up in app. comment below code if you want to see warnings.
useEffect(() => {
LogBox.ignoreAllLogs();
}, []);
if (!isLoadingComplete) {
return null;
} else {
return (
<ThemeProvider>
<SafeAreaProvider>
<Navigation colorScheme={colorScheme} />
<StatusBar />
<Web3Modal
projectId={process.env.REACT_APP_ENV_PROJECT_ID!!}
providerMetadata={providerMetadata}
sessionParams={sessionParams}
/>
</SafeAreaProvider>
</ThemeProvider>
);
}
}
The projectId should be in your environment variables. At the root of your project directory create .env file and place this code there
REACT_APP_ENV_PROJECT_ID = <YOUR PROJECT ID>
You can get the projectId here.
After doing the above you can test your application by simply doing npm start
this will start the development server.
If you are testing with expo ensure you have expo installed on your mobile device and then scan the QR code. This should open the app. And you can start testing the functionalities of what we have done so far.
Generate an APK to Test on Android Device
For testing on other devices we will need to generate a downloadable link, you will need to run the following command;
Follow this setup guide to generate an APK.
NOTE
Ensure you have eas installed globally. If it’s not already installed run this command npm install -g eas-cli
Once it’s installed. If you don’t already have eas.json
file add it to the root of your react-native-app directory and the code should look like this
{
"build": {
"preview": {
"android": {
"buildType": "apk"
}
},
"preview2": {
"android": {
"gradleCommand": ":app:assembleRelease"
}
},
"preview3": {
"developmentClient": true
},
"production": {}
}
}
Run this command to generate a sharable android APK link eas build -p android --profile preview
To test the android version of the app download the apk here
NOTE
To test on IOS device follow the guide here.
Conclusion
Congratulations on finishing this tutorial! Thank you for taking the time to complete it. In this tutorial, you have learnt how to create a React Native DApp that you can use to interact with the Celo blockchain and transfer tokens.
If you have any issue while following this tutorial feel free to drop it in the comment. And if this tutorial helped you or you find it interesting please hit the love button.
Here is the link to the project repo on github.
Next Steps
As a next step, apply what you have learnt in this tutorial and explore the other powerful features of celo composer react native with and without expo. Also extend the functionalities of this dapp to something more advance.
About the Author
Glory Agatevure is a blockchain engineer, technical writer, and co-founder of Africinnovate. You can connect with me on Linkedin, Twitter and Github.