One of the primary features of decentralized applications (DApps) is the ability to connect a wallet, which in turn allows the user to interact with transactions on the DApp. It abstracts functionalities like switching networks, providing signers, and other features that provide users with a form of authentication. Connecting a wallet also acts as a gateway that allows users to make and read actions on the blockchain through the DApp, using their wallet address as an authorized identity.
WalletConnect is a free and open source protocol that makes it possible to connect our DApps to multiple wallets, including MetaMask, Trust Wallet, Rainbow, and others. This protocol abstracts this process by establishing a connection between the DApp and the wallet, keeping them in sync throughout the session.
In this article, we’ll use WalletConnect to link our wallet app to our DApp, using Vue.js on the front end. One thing to note is that WalletConnect can be used on any DApp, chain, and wallet (custodial and non-custodial) that are wallet-connect compatible.
You can find the source code for this tutorial here, and a demo of the app we’ll be building here.
Firstly, let’s use the Vue CLI to kickstart the project. If you already have Vue CLI installed on your system, you can go on to create the Vue project directly.
You can install it globally with this command:
npm install -g @vue/cli
We can now use the Vue CLI to create our project. Create a new project using this command:
vue create vue-wallet-connect
You will need to pick a preset. Choose Manually select features
, then select the options as shown below:
After the project has been created, navigate to the new project folder:
cd vue-wallet-connect
We’ll be using Ethers.js in our Vue app to directly interact with the blockchain while connecting our wallet:
npm i ethers
Here, we install the WalletConnect library into your project:
npm install --save web3 @walletconnect/web3-provider
Next, to use the WalletConnect library directly in Vue 3, we need to install node-polyfill-webpack-plugin
:
npm i node-polyfill-webpack-plugin
We are installing it because our project uses webpack v5, where polyfill Node core modules were removed. So, we are installing it to access these modules in the project.
Now, open the vue.config.js
file and replace it with this block of code:
const { defineConfig } = require("@vue/cli-service"); const NodePolyfillPlugin = require("node-polyfill-webpack-plugin"); module.exports = defineConfig({ transpileDependencies: true, configureWebpack: { plugins: [new NodePolyfillPlugin()], optimization: { splitChunks: { chunks: "all", }, }, }, });
Once that is done you can now start the server:
npm run serve
Let’s go into the components folder and create a new file called StatusContainer.vue
. This component contains our main page.
It it, we have our welcome message, the Connect Wallet button that helps us connect, and our Disconnect button to disconnect us from the wallet. Finally, the Connected button displays when we successfully connect to a wallet:
<template> <div class="hello"> <h1>Welcome to Your Vue.js Dapp</h1> <div > <button class="button">Connected</button> <button class="disconnect__button">Disconnect</button> </div> <button class="button"> Connect Wallet</button> </div> </template> <script> export default { name: 'StatusContainer' } </script>
Once that’s done, open the App.vue
file and import the StatusContainer
component like so:
<template> <status-container/> </template> <script> import StatusContainer from './components/StatusContainer.vue' export default { name: 'App', components: { StatusContainer } } </script> <style> @import url('https://fonts.googleapis.com/css2?family=Sora:wght@100&display=swap'); #app { font-family: 'Sora', sans-serif; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; text-align: center; color: #2c3e50; margin-top: 60px; } .button { background-color: #1c82ff; border: none; color: #ffffff; font-family: "Sora"; border-radius: 3rem; padding: 2rem 3rem; font-weight: 600; font-size: 2rem; margin: 1rem 1rem 1rem auto; width: 40%; } .disconnect__button { background-color: red; border: none; color: #ffffff; font-family: "Sora"; border-radius: 3rem; padding: 1rem 1.3rem; font-weight: 600; font-size: 1rem; margin: 8rem 1rem 1rem auto; width: 20%; } </style>
Within our style tag, we now add styles for the buttons we created earlier: .button
and .disconnect__button
. Also, we import the Sora custom font from Google Fonts and use it as our font-family
.
We’ll be needing an RPC provider to instantiate our WalletConnect library. For this example, we’ll use Infura. Open Infura, create a new project, and grab the Project ID.
Now, create a new walletConnect
folder under the src folder: src/walletConnect
. Within this folder, let’s create a provider.js
file. Here, we import our WalletConnect library, instantiate it using our Infura ID, and export it for use in other files.
src/walletConnect/provider.js
will look like this:
import WalletConnectProvider from "@walletconnect/web3-provider"; export const provider = new WalletConnectProvider({ infuraId: process.env.VUE_APP_INFURA_ID, });
The Infura ID should be used as an environmental variable. So add the following to your .env
file:
VUE_APP_INFURA_ID={{INFURA__ID}}
After creating our interface and successfully instantiating our library, the next step is to implement our functionalities. To do this, we’ll use Vue composables, because it allows us to use our state and actions in any component within the app, similar to what we have with Pinia and Vuex.
Inside the src
folder, add src/composables/connect
. Within the connect
folder, let’s create an index.js
file.
Here, we import reactive
and watch
, which we’ll use in this file. Let’s create our state object called defaultState
:
import { reactive, watch } from "vue"; const defaultState = { address: "", chainId: "", status: false, }; const state = defaultState
In a bid to keep our state consistent, we sync the state with an item in the local storage. Let us name this item "userState"
and assign it to a variable called STATE_NAME
. This is done to avoid making mistakes when repeating "userState"
in multiple places:
const STATE_NAME = "userState";
We now use watch
to update our local storage once there are any changes in our state:
watch( () => state, () => { localStorage.setItem(STATE_NAME, JSON.stringify(state)); }, { deep: true } );
Next, we create a getDefaultState
function that checks if our STATE_NAME
item in the local storage exists and assigns the local storage item to the state. If our local storage item does not exist, it assigns the defaultState
to state
.
Now, we can delete const state = defaultState
and use reactive
to assign const state = reactive(getDefaultState());
:
const getDefaultState = () => { if (localStorage.getItem(STATE_NAME) !== null) { return JSON.parse(localStorage.getItem(STATE_NAME)); } return defaultState; }; const state = reactive(getDefaultState());
Finally, we export our state. We also add an if
statement that checks if our local storage item does not exist. If it doesn’t, it creates the item and assigns state
to local storage:
export default () => { if (localStorage.getItem(STATE_NAME) === null) { localStorage.setItem(STATE_NAME, JSON.stringify(state)); } return { state, }; };
Now, our state always syncs with the local storage, ensuring consistency.
Let’s look at src/composables/connect/index.js
:
import { reactive, watch } from "vue"; const defaultState = { address: "", chainId: "", status: false, }; const STATE_NAME = "userState"; const getDefaultState = () => { if (localStorage.getItem(STATE_NAME) !== null) { return JSON.parse(localStorage.getItem(STATE_NAME)); } return defaultState; }; const state = reactive(getDefaultState()); watch( () => state, () => { localStorage.setItem(STATE_NAME, JSON.stringify(state)); }, { deep: true } ); export default () => { if (localStorage.getItem(STATE_NAME) === null) { localStorage.setItem(STATE_NAME, JSON.stringify(state)); } return { state, }; };
Our actions consist of functions that’ll be using in our app. We’ll be creating three functions:
connectWalletConnect
, which triggers the WalletConnect modal to connect with walletsautoConnect
, which handles consistency within our WalletConnect session after the DApp is connected, so when the DApp is connected and you refresh the page, the user’s session is still activedisconnectWallet
, which disconnects the DApp from the wallet and ends the user’s sessionLet’s jump right into the code!
connectWalletConnect
Still within our connect
folder (src/composables/connect
), create the connectWalletConnect
file. First, we import our index file, providers
from ethers
, and our provider
that we created earlier in our src/walletConnect/provider.js
file:
import { providers } from "ethers"; import connect from "./index"; import { provider } from "../../walletConnect/provider"; const connectWalletConnect = async () => { try { const { state } = connect(); // Enable session (triggers QR Code modal) await provider.enable(); const web3Provider = new providers.Web3Provider(provider); const signer = await web3Provider.getSigner(); const address = await signer.getAddress(); state.status = true; state.address = address; state.chainId = await provider.request({ method: "eth_chainId" }); provider.on("disconnect", (code, reason) => { console.log(code, reason); console.log("disconnected"); state.status = false; state.address = ""; localStorage.removeItem("userState"); }); provider.on("accountsChanged", (accounts) => { if (accounts.length > 0) { state.address = accounts[0]; } }); provider.on("chainChanged", (chainId) => { state.chainId = chainId }); } catch (error) { console.log(error); } }; export default connectWalletConnect;
Next, we have a try-catch
statement. Within our try
statement, we get our state from connect()
and pop up our QR modal for connection. Once connected, we assign our address
and chainId
to the state properties and make our state.status
read true
.
We then watch three events with the provider
: disconnect
, accountsChanged
, and chainChainged
.
disconnect
is triggered once the user disconnects directly from their walletaccountsChanged
is triggered if the user switches accounts in their wallet. If the length of the account
array is greater than zero, we assign our state.address
to the first address in the array (accounts[0]
), which is the current addresschainChainged
is triggered if the user switches their chain/network. For example, if they switch their chain from Ethereum mainnet to rinkeby testnet, our application changes the state.chainId
from 1
to 4
Then, our catch
statement simply logs any error to the console.
Go back into the index.js
file in the connect
folder and import the connectWalletConnect
action. Here, we create an actions
object and export it with our state
:
import { reactive, watch } from "vue"; import connectWalletConnect from "./connectWalletConnect"; const STATE_NAME = "userState"; const defaultState = { address: "", chainId: "", status: false, }; const getDefaultState = () => { if (localStorage.getItem(STATE_NAME) !== null) { return JSON.parse(localStorage.getItem(STATE_NAME)); } return defaultState; }; const state = reactive(getDefaultState()); const actions = { connectWalletConnect, }; watch( () => state, () => { localStorage.setItem(STATE_NAME, JSON.stringify(state)); }, { deep: true } ); export default () => { if (localStorage.getItem(STATE_NAME) === null) { localStorage.setItem(STATE_NAME, JSON.stringify(state)); } return { state, ...actions, }; };
autoConnect
Let’s move on to autoConnect.js
, and our actions
. Similar to connectWalletConnect
, create an autoConnect.js
file. We import the index file and destructure it to get our state
and connectWalletConnect
using connect()
:
import connect from "./index"; const autoConnect = () => { const { state, connectWalletConnect } = connect(); if (state.status) { if (localStorage.getItem("walletconnect") == null) { console.log("disconnected"); console.log("disconnected"); state.status = false; state.address = ""; localStorage.removeItem("userState"); } if (localStorage.getItem("walletconnect")) { (async () => { console.log("start"); connectWalletConnect(); })(); } } }; export default autoConnect;
One thing you should know is that once WalletConnect has successfully connected to a DApp, all the information concerning that wallet (including the address and chain ID) is in the local storage under an item called walletconnect
. Once the session is disconnected, it’s automatically deleted.
autoConnect
checks if our state.status
is true. If so, we check if there’s a walletConnect
item in local storage. If it’s not in the local storage, we delete all existing data in our state and the userState
item in the local storage.
However, if walletconnect
is present in your local storage, we have an async function that ”reactivates” that existing session for our DApp by firing connectWalletConnect();
. So, if we refresh the page, the connection remains active, and can listen to our provider
events.
disconnectWallet
Let’s look at our last action: disconnectWallet
. This action allows us to end the session from the DApp itself.
First, we import our provider and state. Then, we use provider.disconnect();
to disconnect the session, after which we reset the state back to default, and delete the "userState"
item in our local storage:
import { provider } from "../../walletConnect/provider"; import connect from "./index"; const disconnectWallet = async () => { const { state } = connect(); await provider.disconnect(); state.status = false; state.address = ""; localStorage.removeItem("userState"); } export default disconnectWallet;
We can now go back to our src/composables/connect/index.js
and update the actions
object like so:
const actions = { connectWalletConnect, autoConnect, disconnectWallet };
Let’s open our StatusContainer
component and connect the logic in our composables to the interface. As usual, import your composable file and destructure it to get the actions (connect and disconnect) and our state:
<script> import connect from '../composables/connect/index'; export default { name: 'StatusContainer', setup: () => { const { connectWalletConnect, disconnectWallet, state } = connect(); const connectUserWallet = async () => { await connectWalletConnect(); }; const disconnectUser = async() => { await disconnectWallet() } return { connectUserWallet, disconnectUser, state } } } </script>
We then return the functions (disconnectUser
, connectUserWallet
) and state
to be used in the template:
<template> <div class="hello"> <h1>Welcome to Your Vue.js Dapp</h1> <div v-if="state.status"> <button @click="connectUserWallet" class="button">Connected</button> <h3>Address: {{state.address}}</h3> <h3>ChainId: {{state.chainId}}</h3> <button @click="disconnectUser" class="disconnect__button">Disconnect</button> </div> <button v-else @click="connectUserWallet" class="button"> Connect Wallet</button> </div> </template>
First, we use v-if
to display things conditionally, using state.status
. If we’re connected and state.status
is true, we display the Connected button, the user address
, and chainId
. Also, we’ll display a Disconnect button that triggers our disconnectUser
function.
If the user is not connected and state.status
is false
, we display only the Connect Wallet button that triggers our connectUserWallet
function when we click.
Let’s go into our App.vue
component and add our autoConnect
logic to the component. Similarly to what we have done before, we import our composable and destructure it to get out autoConnect
action. Using Vue’s onMounted
, we will fire the autoConnect()
function. As mentioned earlier, this allows us to listen to live events from the wallet even we refresh the page:
<script> import StatusContainer from './components/StatusContainer.vue' import connect from './composables/connect/index'; import {onMounted} from "vue"; export default { name: 'App', components: { StatusContainer }, setup: () => { const { autoConnect} = connect(); onMounted(async () => { await autoConnect() }) } } </script>
Congratulations if you made it all the way here! 🎉
In this article, we covered step-by-step details on implementing WalletConnect in your Vue DApps. From setting up our project with the correct config and building our interface, to writing the necessary logic to ensure our app is always in sync with the wallet.
Debugging Vue.js applications can be difficult, especially when there are dozens, if not hundreds of mutations during a user session. If you’re interested in monitoring and tracking Vue mutations for all of your users in production, try LogRocket.
LogRocket is like a DVR for web and mobile apps, recording literally everything that happens in your Vue apps, including network requests, JavaScript errors, performance problems, and much more. Instead of guessing why problems happen, you can aggregate and report on what state your application was in when an issue occurred.
The LogRocket Vuex plugin logs Vuex mutations to the LogRocket console, giving you context around what led to an error and what state the application was in when an issue occurred.
Modernize how you debug your Vue apps — start monitoring for free.
Would you be interested in joining LogRocket's developer community?
Join LogRocket’s Content Advisory Board. You’ll help inform the type of content we create and get access to exclusive meetups, social accreditation, and swag.
Sign up nowMaking carousels can be time-consuming, but it doesn’t have to be. Learn how to use React Snap Carousel to simplify the process.
Consider using a React form library to mitigate the challenges of building and managing forms and surveys.
In this article, you’ll learn how to set up Hoppscotch and which APIs to test it with. Then we’ll discuss alternatives: OpenAPI DevTools and Postman.
Learn to migrate from react-native-camera to VisionCamera, manage permissions, optimize performance, and implement advanced features.