Building an Ethereum Firebase User profile Dapp (Part 2)

Sebastián Brocher
15 min readApr 19, 2018

Using Truffle, Web3, Solidity, Firebase, and Vue/Vuex/Vuetify

On part 1 of this tutorial, we got the project started and setup the stack with Truffle, Firebase, and Vue/Vuex/Vuetify. On this part 2, we’ll have a bit more fun implementing user authentication, better MetaMask handling, and more.

Inspiration and credits

There’s some great resources out there already that implement different pieces of this tutorial (authentication with Vuex & Firebase, Vue + Ethereum, etc). Here’re some of the sources I’ve reviewed and took inspiration from:

The code

You can clone the final code for this project from github. Note that I recommend doing that over copying and pasting each step below.

Implementing user authentication with Firebase & Vue

Let’s start by borrowing from some of the projects above to get the authentication feature going. I like how the ‘vue-firebase-auth-vuex’ project organizes things, so we’ll borrow most of the UI and Vue/Firebase code from that. First, let’s go ahead and get our components setup. Change src/App.vue to the following content:

<template>
<v-app>
<v-navigation-drawer app temporary v-model="sideNav">
<v-list>
<v-list-tile
v-for="item in menuItems"
:key="item.title"
:to="item.link">
<v-list-tile-action>
<v-icon>{{ item.icon }}</v-icon>
</v-list-tile-action>
<v-list-tile-content>{{ item.title }}</v-list-tile-content>
</v-list-tile>
<v-list-tile
v-if="userIsAuthenticated"
@click="onLogout">
<v-list-tile-action>
<v-icon>exit_to_app</v-icon>
</v-list-tile-action>
<v-list-tile-content>Logout</v-list-tile-content>
</v-list-tile>
</v-list>
</v-navigation-drawer>
<v-toolbar dark class="primary">
<v-toolbar-side-icon
@click.stop="sideNav = !sideNav"
class="hidden-sm-and-up "></v-toolbar-side-icon>
<v-toolbar-title>
<router-link to="/" tag="span" style="cursor: pointer">Ethereum Firebase App</router-link>
</v-toolbar-title>
<v-spacer></v-spacer>
<v-toolbar-items class="hidden-xs-only">
<v-btn
flat
v-for="item in menuItems"
:key="item.title"
:to="item.link">
<v-icon left dark>{{ item.icon }}</v-icon>
{{ item.title }}
</v-btn>
<v-btn
v-if="userIsAuthenticated"
flat
@click="onLogout">
<v-icon left dark>exit_to_app</v-icon>
Logout
</v-btn>
</v-toolbar-items>
</v-toolbar>
<main app>
<router-view></router-view>
</main>
</v-app>
</template>
<script>
export default {
data () {
return {
sideNav: false
}
},
computed: {
menuItems () {
let menuItems = [
{icon: 'face', title: 'Sign up', link: '/signup'},
{icon: 'lock_open', title: 'Sign in', link: '/signin'}
]
if (this.userIsAuthenticated) {
menuItems = [
{icon: 'person', title: 'Profile', link: '/profile'}
]
}
return menuItems
},
userIsAuthenticated () {
return this.$store.getters.user !== null && this.$store.getters.user !== undefined
}
},
methods: {
onLogout () {
this.$store.dispatch('logout')
this.$router.push('/')
}
}
}
</script>

Remove the HelloWorld.vue component example that came with the vuex skeleton:

rm src/components/HelloWorld.vue

Now create src/components/Home.vue with:

<template>
<v-container>
<v-layout>
<v-flex xs12 sm6 offset-sm3>
<v-card>
<v-card-title primary-title>
<div>
<h3 class="headline mb-0">Welcome to Ethereum Firebase App.</h3>
<div></div>
</div>
</v-card-title>
</v-card>
</v-flex>
</v-layout>
</v-container>
</template>

And src/components/Shared/Alert.vue with:

<template>
<v-alert error dismissible @input="onClose" :value="true">
{{ text }}
</v-alert>
</template>
<script>
export default {
props: ['text'],
methods: {
onClose () {
this.$emit('dismissed')
}
}
}
</script>

And src/components/User/Signup.vue with:

<template>
<v-container>
<v-layout row v-if="error">
<v-flex xs12 sm6 offset-sm3>
<app-alert @dismissed="onDismissed" :text="error.message"></app-alert>
</v-flex>
</v-layout>
<v-layout row>
<v-flex xs12 sm6 offset-sm3>
<v-card>
<v-card-text>
<v-container>
<form @submit.prevent="onSignup">
<v-layout row>
<v-flex xs12>
<v-text-field
name="email"
label="Mail"
id="email"
v-model="email"
type="email"
required></v-text-field>
</v-flex>
</v-layout>
<v-layout row>
<v-flex xs12>
<v-text-field
name="password"
label="Password"
id="password"
v-model="password"
type="password"
required></v-text-field>
</v-flex>
</v-layout>
<v-layout row>
<v-flex xs12>
<v-text-field
name="confirmPassword"
label="Confirm Password"
id="confirmPassword"
v-model="confirmPassword"
type="password"
:rules="[comparePasswords]"></v-text-field>
</v-flex>
</v-layout>
<v-layout row>
<v-flex xs12>
<div class="text-xs-center">
<v-btn round type="submit" :disabled="loading" :loading="loading">
Sign up
<v-icon right>lock_open</v-icon>
<span slot="loader" class="custom-loader">
<v-icon light>cached</v-icon>
</span>
</v-btn>
</div>
<div class="text-xs-center">
<v-btn round class="red" dark :disabled="loading" :loading="loading" @click.prevent="onSigninGoogle">Login with Google
<v-icon right dark>lock_open</v-icon>
<span slot="loader" class="custom-loader">
<v-icon light>cached</v-icon>
</span>
</v-btn>
</div>
<div class="text-xs-center">
<v-btn round primary dark :disabled="loading" :loading="loading" @click.prevent="onSigninFacebook">Login with Facebook
<v-icon right dark>lock_open</v-icon>
<span slot="loader" class="custom-loader">
<v-icon light>cached</v-icon>
</span>
</v-btn>
</div>
<div class="text-xs-center">
<v-btn round dark :disabled="loading" :loading="loading" @click.prevent="onSigninGithub">Login with Github
<v-icon right dark>lock_open</v-icon>
<span slot="loader" class="custom-loader">
<v-icon light>cached</v-icon>
</span>
</v-btn>
</div>
<div class="text-xs-center">
<v-btn round info dark :disabled="loading" :loading="loading" @click.prevent="onSigninTwitter">Login with Twitter
<v-icon right dark>lock_open</v-icon>
<span slot="loader" class="custom-loader">
<v-icon light>cached</v-icon>
</span>
</v-btn>
</div>
</v-flex>
</v-layout>
</form>
</v-container>
</v-card-text>
</v-card>
</v-flex>
</v-layout>
</v-container>
</template>
<script>
export default {
data () {
return {
email: '',
password: '',
confirmPassword: ''
}
},
computed: {
comparePasswords () {
return this.password !== this.confirmPassword ? 'Passwords do not match' : ''
},
user () {
return this.$store.getters.user
},
error () {
return this.$store.getters.error
},
loading () {
return this.$store.getters.loading
}
},
watch: {
user (value) {
if (value !== null && value !== undefined) {
this.$router.push('/profile')
}
}
},
methods: {
onSignup () {
this.$store.dispatch('signUserUp', {email: this.email, password: this.password})
},
onSigninGoogle () {
this.$store.dispatch('signUserInGoogle')
},
onSigninFacebook () {
this.$store.dispatch('signUserInFacebook')
},
onSigninGithub () {
this.$store.dispatch('signUserInGithub')
},
onSigninTwitter () {
this.$store.dispatch('signUserInTwitter')
},
onDismissed () {
this.$store.dispatch('clearError')
}
}
}
</script>

And src/components/User/Signin.vue with:

<template>
<v-container>
<v-layout row v-if="error">
<v-flex xs12 sm6 offset-sm3>
<app-alert @dismissed="onDismissed" :text="error.message"></app-alert>
</v-flex>
</v-layout>
<v-layout row>
<v-flex xs12 sm6 offset-sm3>
<v-card>
<v-card-text>
<v-container>
<form @submit.prevent="onSignin">
<v-layout row>
<v-flex xs12>
<v-text-field
name="email"
label="Mail"
id="email"
v-model="email"
type="email"
required></v-text-field>
</v-flex>
</v-layout>
<v-layout row>
<v-flex xs12>
<v-text-field
name="password"
label="Password"
id="password"
v-model="password"
type="password"
required></v-text-field>
</v-flex>
</v-layout>
<v-layout row>
<v-flex xs12>
<div class="text-xs-center">
<v-btn round type="submit" :disabled="loading" :loading="loading">
Sign in
<v-icon right>lock_open</v-icon>
<span slot="loader" class="custom-loader">
<v-icon light>cached</v-icon>
</span>
</v-btn>
</div>
<div class="text-xs-center">
<v-btn round class="red" dark :disabled="loading" :loading="loading" @click.prevent="onSigninGoogle">Login with Google
<v-icon right dark>lock_open</v-icon>
<span slot="loader" class="custom-loader">
<v-icon light>cached</v-icon>
</span>
</v-btn>
</div>
<div class="text-xs-center">
<v-btn round primary dark :disabled="loading" :loading="loading" @click.prevent="onSigninFacebook">Login with Facebook
<v-icon right dark>lock_open</v-icon>
<span slot="loader" class="custom-loader">
<v-icon light>cached</v-icon>
</span>
</v-btn>
</div>
<div class="text-xs-center">
<v-btn round dark :disabled="loading" :loading="loading" @click.prevent="onSigninGithub">Login with Github
<v-icon right dark>lock_open</v-icon>
<span slot="loader" class="custom-loader">
<v-icon light>cached</v-icon>
</span>
</v-btn>
</div>
<div class="text-xs-center">
<v-btn round info dark :disabled="loading" :loading="loading" @click.prevent="onSigninTwitter">Login with Twitter
<v-icon right dark>lock_open</v-icon>
<span slot="loader" class="custom-loader">
<v-icon light>cached</v-icon>
</span>
</v-btn>
</div>
</v-flex>
</v-layout>
</form>
</v-container>
</v-card-text>
</v-card>
</v-flex>
</v-layout>
</v-container>
</template>
<script>
export default {
data () {
return {
email: '',
password: ''
}
},
computed: {
user () {
return this.$store.getters.user
},
error () {
return this.$store.getters.error
},
loading () {
return this.$store.getters.loading
}
},
watch: {
user (value) {
if (value !== null && value !== undefined) {
this.$router.push('/profile')
}
}
},
methods: {
onSignin () {
this.$store.dispatch('signUserIn', {email: this.email, password: this.password})
},
onSigninGoogle () {
this.$store.dispatch('signUserInGoogle')
},
onSigninFacebook () {
this.$store.dispatch('signUserInFacebook')
},
onSigninGithub () {
this.$store.dispatch('signUserInGithub')
},
onSigninTwitter () {
this.$store.dispatch('signUserInTwitter')
},
onDismissed () {
this.$store.dispatch('clearError')
}
}
}
</script>

And src/components/User/Profile.vue with:

<template>
<v-container>
<v-layout>
<v-flex xs12 sm6 offset-sm3>
<v-card>
<v-card-title primary-title>
<div class="text-md-center">
<h2>Login Success</h2>
<h4 class="headline mb-0">{{ user.name }}</h4>
<h4 class="headline mb-0">{{ user.email }}</h4>
</div>
</v-card-title>
</v-card>
</v-flex>
</v-layout>
</v-container>
</template>
<script>
export default {
computed: {
user () {
return this.$store.getters.user
}
}
}
</script>

Now let’s get routing configured. Go ahead and change src/router/index.js contents to:

import Vue from 'vue'
import Router from 'vue-router'
import AuthGuard from './auth-guard'
const Home = () => import('@/components/Home')
const Profile = () => import('@/components/User/Profile')
const Signup = () => import('@/components/User/Signup')
const Signin = () => import('@/components/User/Signin')
Vue.use(Router)export default new Router({
routes: [
{
path: '/',
name: 'Home',
component: Home
},
{
path: '/profile',
name: 'Profile',
component: Profile,
beforeEnter: AuthGuard
},
{
path: '/signup',
name: 'Signup',
component: Signup
},
{
path: '/signin',
name: 'Signin',
component: Signin
}
],
mode: 'history'
})

And create src/router/auth-guard.js with:

import {store} from '../store'export default (to, from, next) => {
if (store.getters.user) {
next()
} else {
next('/signin')
}
}

Now let’s setup the store. Change src/store.index.js with:

import Vue from 'vue'
import Vuex from 'vuex'
import user from './user'
import shared from './shared'
Vue.use(Vuex)export const store = new Vuex.Store({
modules: {
user: user,
shared: shared
}
})

Create src/store/shared/index.js with:

export default {
state: {
loading: false,
error: null
},
mutations: {
setLoading (state, payload) {
state.loading = payload
},
setError (state, payload) {
state.error = payload
},
clearError (state) {
state.error = null
}
},
actions: {
clearError ({commit}) {
commit('clearError')
}
},
getters: {
loading (state) {
return state.loading
},
error (state) {
return state.error
}
}
}

And create src/store/user/index.js with:

import * as firebase from 'firebase'export default {
state: {
user: null
},
mutations: {
setUser (state, payload) {
state.user = payload
}
},
actions: {
signUserUp ({commit}, payload) {
commit('setLoading', true)
commit('clearError')
firebase.auth().createUserWithEmailAndPassword(payload.email, payload.password)
.then(
user => {
commit('setLoading', false)
const newUser = {
id: user.uid,
name: user.displayName,
email: user.email,
photoUrl: user.photoURL
}
commit('setUser', newUser)
}
)
.catch(
error => {
commit('setLoading', false)
commit('setError', error)
console.log(error)
}
)
},
signUserIn ({commit}, payload) {
commit('setLoading', true)
commit('clearError')
firebase.auth().signInWithEmailAndPassword(payload.email, payload.password)
.then(
user => {
commit('setLoading', false)
const newUser = {
id: user.uid,
name: user.displayName,
email: user.email,
photoUrl: user.photoURL
}
commit('setUser', newUser)
}
)
.catch(
error => {
commit('setLoading', false)
commit('setError', error)
console.log(error)
}
)
},
signUserInGoogle ({commit}) {
commit('setLoading', true)
commit('clearError')
firebase.auth().signInWithPopup(new firebase.auth.GoogleAuthProvider())
.then(
user => {
commit('setLoading', false)
const newUser = {
id: user.uid,
name: user.displayName,
email: user.email,
photoUrl: user.photoURL
}
commit('setUser', newUser)
}
)
.catch(
error => {
commit('setLoading', false)
commit('setError', error)
console.log(error)
}
)
},
signUserInFacebook ({commit}) {
commit('setLoading', true)
commit('clearError')
firebase.auth().signInWithPopup(new firebase.auth.FacebookAuthProvider())
.then(
user => {
commit('setLoading', false)
const newUser = {
id: user.uid,
name: user.displayName,
email: user.email,
photoUrl: user.photoURL
}
commit('setUser', newUser)
}
)
.catch(
error => {
commit('setLoading', false)
commit('setError', error)
console.log(error)
}
)
},
signUserInGithub ({commit}) {
commit('setLoading', true)
commit('clearError')
firebase.auth().signInWithPopup(new firebase.auth.GithubAuthProvider())
.then(
user => {
commit('setLoading', false)
const newUser = {
id: user.uid,
name: user.displayName,
email: user.email,
photoUrl: user.photoURL
}
commit('setUser', newUser)
}
)
.catch(
error => {
commit('setLoading', false)
commit('setError', error)
console.log(error)
}
)
},
signUserInTwitter ({commit}) {
commit('setLoading', true)
commit('clearError')
firebase.auth().signInWithPopup(new firebase.auth.TwitterAuthProvider())
.then(
user => {
commit('setLoading', false)
const newUser = {
id: user.uid,
name: user.displayName,
email: user.email,
photoUrl: user.photoURL
}
commit('setUser', newUser)
}
)
.catch(
error => {
commit('setLoading', false)
commit('setError', error)
console.log(error)
}
)
},
autoSignIn ({commit}, payload) {
commit('setUser', {
id: payload.uid,
name: payload.displayName,
email: payload.email,
photoUrl: payload.photoURL
})
},
logout ({commit}) {
firebase.auth().signOut()
commit('setUser', null)
}
},
getters: {
user (state) {
return state.user
}
}
}

Finally, let’s update our app’s javascript entry point (src/main.js) to this:

new Vue({
el: '#app',
router,
store,
render: h => h(App),
created () {
firebase.initializeApp({
<your firebase credentials>
})
firebase.auth().onAuthStateChanged((user) => {
if (user) {
this.$store.dispatch('autoSignIn', user)
}
})
}

Make sure to replace <your firebase credentials> above (refer to Part 1 for more details on how to do that).

Alright! That was a lot of code to get the feature setup. Let’s have a look and make sure it works. Go ahead and start your dev server with ‘npm dev run’ and open your browser. You should see something like this:

Great! Before testing signing up, go to your Firebase console and make sure you add all the different auth providers you want to use (otherwise you’ll get an error telling you the auth-provider is disabled for the project when you try to sign up or sign in). Make sure you test your sign up, sign in, and profile pages, everything should be working by now! And, if you try /profile without being signed in, you should be prompted to sign in.

Improving MetaMask support

The original code in this section comes from this post: Create your first Ethereum dAPP with Web3 and Vue.JS. I adapted it to fit our needs and our project structure.

Let’s improve how we handle the UX with regards to MetaMask with the following:

  • Detect MetaMask status (installed and available, network ID , address, and balance).
  • Display a visual clue with status above in the nav bar

To implement the above, let’s create a MetaMask component. Create src/components/Shared/MetaMask.vue with the following:

<template>
<p>Test</p>
</template>
<script>
export default {
methods: {}
}
</script>

Then let’s call that component from src/App.vue. Insert the following lines after <v-toolbar-title>:

<v-spacer></v-spacer>
<app-metamask></app-metamask>

We also need to register the component. Add the following lines to src/main.js, following the same pattern as with our previous components:

const MetaMaskCmp = () => import('./components/Shared/MetaMask.vue')
...
Vue.component('app-metamask', MetaMaskCmp)

Now let’s create a store module. Create src/store/metamask folder, and src/store/metamask/index.js file with the following content:

export default {
state: {
web3: {
isInjected: false,
web3Instance: null,
networkId: null,
coinbase: null,
balance: null,
error: null
},
contractInstance: null
}
}

And refer to this store from the main src/store/index.js file by adding the following:

...
import metamask from './metamask'
...
shared: shared,
metamask: metamask
...

Now we’ll borrow the utility code for web3 from the post above. Create src/utils folder, and then src/utils/getWeb3.js with the code from the post. Now let’s use it from /src/components/metamask/index.js by adding the following lines:

import getWeb3 from '../../utils/getWeb3'
...
actions: {
registerWeb3 ({commit}) {
console.log('registerWeb3 Action being executed')
getWeb3.then(result => {
console.log('committing result to registerWeb3Instance mutation')
commit('registerWeb3Instance', result)
}).catch(e => {
console.log('error in action registerWeb3', e)
})
}
},
mutations: {
registerWeb3Instance (state, payload) {
console.log('registerWeb3instance Mutation being executed', payload)
let result = payload
let web3Copy = state.web3
web3Copy.coinbase = result.coinbase
web3Copy.networkId = result.networkId
web3Copy.balance = parseInt(result.balance, 10)
web3Copy.isInjected = result.injectedWeb3
web3Copy.web3Instance = result.web3
state.web3 = web3Copy
}
}
...

Finally, let’s trigger that with a beforeCreate hook on App.vue:

...
beforeCreate () {
console.log('registerWeb3 Action dispatched from App.vue')
this.$store.dispatch('registerWeb3')
}

You may remember we said earlier we were going to move around the web3 connection. We actually just replaced our old method, so let’s go ahead and remove it from src/main.js (just remove the window.AddEventListener block). You can also remove the import web3 line from that file if you’d like.

Alright, so now the final step is to populate the component with the data from the store. To do so, update src/components/Shared/MetaMask.vue with:

<template>
<v-container fluid class="pa-0">
<v-flex>
<div class="text-xs-center">
<v-chip light v-if="!web3.isInjected || !web3.networkId" color="orange" text-color="white">
<v-avatar>
<v-icon medium>cancel</v-icon>
</v-avatar>
MetaMask not connected
</v-chip>
<v-chip light v-if="web3.isInjected && web3.networkId" color="green" text-color="white">
<v-avatar>
<v-icon small>check</v-icon>
</v-avatar>
MetaMask connected (address: {{ web3.coinbase }})
</v-chip>
</div>
</v-flex>
</v-container>
</template><script>
export default {
name: 'app-metamask',
computed: {
web3 () {
return this.$store.state.metamask.web3
}
}
}
</script>

That was quite a bit of code. Let’s make sure it works. Open up your dev server. When MetaMask is not installed, or if you’re not signed in, your nav bar should look something like this:

Now go ahead and sign in to MetaMask, then refresh your browser. You should now see something like this instead:

Did you notice how we had to refresh the browser to have our app reflect the current MetaMask status? That’s not a great UX, so let’s improve it to automatically reflect changes.

Tip: Checkout MetaMask’s Developer’s FAQ / Compatibility Guide for more info on how to do this.

As per the reference above, we need to poll the status from MetaMask from time to time (using setInterval) to do this. Create /src/utils/pollWeb3.js with the following content:

import Web3 from 'web3'
import {store} from '../store/'
let pollWeb3 = function (state) {
let web3 = window.web3
web3 = new Web3(web3.currentProvider)
setInterval(() => {
if (web3 && store.state.metamask.web3.web3Instance) {
if (web3.eth.coinbase !== store.state.metamask.web3.coinbase) {
let newCoinbase = web3.eth.coinbase
web3.eth.getBalance(web3.eth.coinbase, function (err, newBalance) {
if (err) {
console.log(err)
} else {
store.dispatch('pollWeb3', {
coinbase: newCoinbase,
balance: parseInt(newBalance, 10)
})
}
})
} else {
web3.eth.getBalance(store.state.metamask.web3.coinbase, (err, polledBalance) => {
if (err) {
console.log(err)
} else if (parseInt(polledBalance, 10) !== store.state.metamask.web3.balance) {
store.dispatch('pollWeb3', {
coinbase: store.state.metamask.web3.coinbase,
balance: polledBalance
})
}
})
}
}
}, 500)
}
export default pollWeb3

Now open up src/store/metamask/index.js and replace it with the code below (adds registering pollWeb3 to poll, which in turns call new actions and mutations to reflect updated state on the UI):

import getWeb3 from '../../utils/getWeb3'
import pollWeb3 from '../../utils/pollWeb3'
export default {
state: {
web3: {
isInjected: false,
web3Instance: null,
networkId: null,
coinbase: null,
balance: null,
error: null
},
contractInstance: null
},
actions: {
registerWeb3 ({commit}) {
console.log('registerWeb3 Action being executed')
getWeb3.then(result => {
console.log('committing result to registerWeb3Instance mutation')
commit('registerWeb3Instance', result)
}).catch(e => {
console.log('error in action registerWeb3', e)
})
},
pollWeb3 ({commit}, payload) {
console.log('pollWeb3 action being executed')
commit('pollWeb3Instance', payload)
}
},
mutations: {
registerWeb3Instance (state, payload) {
console.log('registerWeb3instance Mutation being executed', payload)
let result = payload
let web3Copy = state.web3
web3Copy.coinbase = result.coinbase
web3Copy.networkId = result.networkId
web3Copy.balance = parseInt(result.balance, 10)
web3Copy.isInjected = result.injectedWeb3
web3Copy.web3Instance = result.web3
state.web3 = web3Copy
pollWeb3()
},
pollWeb3Instance (state, payload) {
console.log('pollWeb3Instance mutation being executed', payload)
state.web3.coinbase = payload.coinbase
state.web3.balance = parseInt(payload.balance, 10)
}
}
}

Go ahead and open up your browser again. Then sign into MetaMask. The UI should automatically refresh without refreshing the browser window. Awesome! BTW, the UI should also automatically reflect any changes on networks, accounts (selected address), and balance.

Adding the user’s Ethereum address to Sign up & Firebase

Now that we have a working Firebase-based authentication and MetaMask integration, let’s combine them a little bit. In particular, we’d like to:

  • Display the user’s Ethereum address on the sign up form
  • Store the user’s Ethereum address in her Firebase profile
  • Display the stored address on the profile component

First, let’s modify the Signup component. Open up src/components/Signup.vue and add the new field:

...
<v-layout row>
<v-flex xs12>
<v-text-field
name="eth_address"
label="Ethereum Address"
id="eth_address"
v-model="eth_address"
type="text"
:disabled="true"
required>
</v-text-field>
</v-flex>
</v-layout>

...

Then add this computed property that returns the address from our new MetaMask component:

...
eth_address () {
return this.$store.state.metamask.web3.coinbase
}

...

And add the eth_address argument to the onSignup method:

this.$store.dispatch('signUserUp', {email: this.email, password: this.password, eth_address: this.eth_address})

Also, let’s comment out the different social media sign up methods from Signup.vue, we wont be using them for this section (you can also remove the corresponding methods and code from the other files too if you’d like). Also comment out the social media sign in methods from Signin.vue.

Then modify src/components/User/Profile to display the address as well:

...
<div class="text-md-center">
<h2>Login Success</h2>
<h4 class="headline mt-2">
Email:
{{ user.email }}
</h4>
<h4 class="headline mt-2">
Ethereum Address: {{ user.name }}
</h4>

</div>
...

Tip: You may have realized we’re using the user’s name field to store the Ethereum address. This is hackish, so use this technique with caution. A better way of doing this is to save the user’s profile data separately on Firebase, but that’s beyond the scope of this tutorial. For our purposes, the name field will work just fine.

Lastly, open up src/store/user.index.js and change the signUserUp action to update the user’s profile with her Ethereum address:

...
signUserUp ({commit}, payload) {
commit('setLoading', true)
commit('clearError')
firebase.auth().createUserWithEmailAndPassword(payload.email, payload.password)
.then(
user => {
user.updateProfile({displayName: payload.eth_address})
.then(
user => {
commit('setLoading', false)
const newUser = {
id: user.uid,
name: user.eth_address,
email: user.email,
photoUrl: user.photoURL
}
commit('setUser', newUser)
}
)
.catch(
error => {
commit('setLoading', false)
commit('setError', error)
console.log(error)
}
)
}
)
.catch(
error => {
commit('setLoading', false)
commit('setError', error)
console.log(error)
}
)
},
...

And that’s it! Open up your browser, connect MetaMask, and go to Sign up. The form should look something like this:

Note that — thanks to our polling mechanism working in conjunction with the computed property — if you change accounts on MetaMask the form address automatically updates. Hit Sign Up, and you should see something like this:

An Important Security Note

Please note we didn’t validate the user does actually have control of the Ethereum address, so this is not a secure implementation. To prove the user actually has the private key corresponding to the address, we would have to either request the user to sign a string, or perform a transaction coming from that address. Depending on how your project uses the address, you could verify it at different times using one of these two methods.We won’t cover this here, so just keep in mind you should not take the address as a verified field at this time.

Interacting with a Smart Contract

We won’t go into the details of adding a smart contract and interacting with it in this tutorial. However, you should be able to easily add this by combining my previous tutorial on building a fully decentralized app and this post.

Final thoughts

As usual, please note this code is not production ready. Feel free to use the code at your own risk. I hope that you’ve enjoyed this and my previous tutorials, and that the project code and examples may be useful to jumpstart your Dapp development. Thanks for reading!

Update: If you enjoyed this tutorial, checkout my post on “Pre-Launching CryptoArte”, the project leverages and uses a lot of the techniques covered here!

--

--