In our last post, we discussed how to upload files to Firestore and query a real-time server to fetch images and display them in the app. I have gone a step further and integrated Firebase auth service so that whenever a user uploads a photo, they have to be authenticated, and the posts collection will contain the reference of the user's unique identifier uid.

You can find the complete source code up to this point at this Github repo release.

By the end of this tutorial, you are going to complete another screen called Profile. Using the Firebase backend and integrated authentication service, you will be able to upload a user's avatar as well as fetch all of the user's posts and display them.

Add user ID to upload post

One of the important changes to Firebase queries that I have made since the previous post is to add a reference to the user's uid. A user's profile screen will display posts based on this uid. This not relevant to the Feed screen since it shows all posts (as in real apps, the feed shows posts from other followed users).

Open src/utils/Firebase.js and modify the uploadPost() function. It gets the current user's uid from the object returned by firebase.auth().currentUser. This is how the current user object from Firebase looks like:

{
  "displayName": null,
  "email": "test@crowdbotics.com",
  "emailVerified": false,
  "isAnonymous": false,
  "metadata": {
    "creationTime": 1573196213572,
    "lastSignInTime": 1573196213572
  },
  "phoneNumber": null,
  "photoURL": null,
  "providerData": [[Object]],
  "providerId": "firebase",
  "refreshToken": "AEu4IL064RJkHKhU0e3pAjS49hmio4RgkkpgvMXFN7tXTTZcP2PffS1dc57hy2RJQPgMstQWP_LojWUfUsAhDqMWQirlztOKHVuWQgbM27WruKWfsq79KEUkmqyhU2oi-_fJhQazWFomnUfHekuUqGjVZSXGjBS4fOBLbK2GAKIpo6PZYQXZ97jxDvA-cBM3TV8HwH8ak4b-",
  "uid": "mrBOaRvdrJPsLkHgJ0uoVI0WyO33"
}

Back to the modification:

uploadPost: post => {
 // add this
 let user = firebase.auth().currentUser
 const id = uuid.v4()
 const uploadData = {
 // add this
 uid: user.uid,
 id: id,
 postPhoto: post.photo,
 postTitle: post.title,
 postDescription: post.description,
 likes: []
 }
 return firebase
 .firestore()
 .collection('posts')
 .doc(id)
 .set(uploadData)
 },

Here is an example of how it looks in the Firestore document.

Firestore document with newly added uid

Fetching User Posts based on ID

Under a user's profile, you'd only want to show posts that are uploaded by the user, unlike the Feed screen. To do this, start by adding a new query method getUserPosts in the src/utils/Firebase.js file.

This method will start by getting the current user. You can then add a where() method to filter only those posts that contain a field of uid of the same user who is currently logged in the app.

getUserPosts: () => {
  let user = firebase.auth().currentUser
  return firebase
    .firestore()
    .collection('posts')
    .where('uid', '==', user.uid)
    .get()
    .then(function(querySnapshot) {
      let posts = querySnapshot.docs.map(doc => doc.data())
      return posts
    })
    .catch(function(error) {
      console.log('Error getting documents: ', error)
    })
}

The rest is the same as getting all the documents from posts collection.

Create a User Profile

To create a user profile, open src/screens/Profile.js. Right now, let's use a static user avatar. To get started, import the following statements.

import React, { Component } from 'react'
import { View } from 'react-native'
import { Text, Button, withStyles, Avatar } from 'react-native-ui-kitten'
import { withFirebaseHOC } from '../utils'
import Gallery from '../components/Gallery'

Next, create a class component _Profile that has a state variable of images whose value is an empty array. Using this state variable you can later fetch only each photo's uri and send it to props to a grid component.

class _Profile extends Component {
  state = {
    images: []
  }

  // ...
}

Before you add logic and create UI for this screen, add the following snippet. This wraps the _Profile class component with a higher order function that allows this component to use Firebase query functions. Also, add necessary styles with another higher order function called withStyles provided by UI Kitten.

export default Profile = withFirebaseHOC(
  withStyles(_Profile, theme => ({
    root: {
      backgroundColor: theme['color-basic-100'],
      marginTop: 60
    },
    header: {
      alignItems: 'center',
      paddingTop: 25,
      paddingBottom: 17
    },
    userInfo: {
      flexDirection: 'row',
      paddingVertical: 18
    },
    bordered: {
      borderBottomWidth: 1,
      borderColor: theme['color-basic-400']
    },
    section: {
      flex: 1,
      alignItems: 'center'
    },
    space: {
      marginBottom: 3,
      color: theme['color-basic-1000']
    },
    separator: {
      backgroundColor: theme['color-basic-400'],
      alignSelf: 'center',
      flexDirection: 'row',
      flex: 0,
      width: 1,
      height: 42
    },
    buttons: {
      flexDirection: 'row',
      paddingVertical: 8
    },
    button: {
      flex: 1,
      alignSelf: 'center'
    },
    text: {
      color: theme['color-basic-1000']
    }
  }))
)

Go back to the _Profile class to add a function that fetches the post from the query you wrote in the previous section. This function will only add the URI of each image in the state variable images.

It is going to be an async function, so to handle promises gracefully, a try/catch block is used.

fetchPosts = async () => {
  try {
    const posts = await this.props.firebase.getUserPosts()
    let images = posts.map(item => {
      return item.postPhoto
    })

    this.setState({ images })
    console.log(this.state.images)
  } catch (error) {
    console.log(error)
  }
}

As soon as the Profile screen renders, this handler method should run. Thus, use a lifecycle method componentDidMount.

componentDidMount() {
 this.fetchPosts()
 }

Next, go to the render function and destructure the state object as well as props. The themedStyle is a prop provided UI kitten library.

render() {
 const { images } = this.state
 const { themedStyle } = this.props
 // ...
 }

The render function is going to return the JSX to display the profile screen. This screen contains a user avatar (which is coming from static image resource URL), a section that shows several posts, follower count, and following count. The demo app does not have the latter two in the database as of now.

The following section is going to show two buttons and, after that, another component called Gallery that will display a grid of images.

return (
  <View style={themedStyle.root}>
    <View style={[themedStyle.header, themedStyle.bordered]}>
      <Avatar
        source={{
          uri:
            'https://images.unsplash.com/photo-1559526323-cb2f2fe2591b?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=1050&q=80'
        }}
        size='giant'
        style={{ width: 100, height: 100 }}
      />
      <Text category='h6' style={themedStyle.text}>
        Test User
      </Text>
    </View>
    <View style={[themedStyle.userInfo, themedStyle.bordered]}>
      <View style={themedStyle.section}>
        <Text category='s1' style={themedStyle.space}>
          {images.length}
        </Text>
        <Text appearance='hint' category='s2'>
          Posts
        </Text>
      </View>
      <View style={themedStyle.section}>
        <Text category='s1' style={themedStyle.space}>
          0
        </Text>
        <Text appearance='hint' category='s2'>
          Followers
        </Text>
      </View>
      <View style={themedStyle.section}>
        <Text category='s1' style={themedStyle.space}>
          0
        </Text>
        <Text appearance='hint' category='s2'>
          Following
        </Text>
      </View>
    </View>
    <View style={themedStyle.buttons}>
      <Button
        style={themedStyle.button}
        appearance='ghost'
        status='danger'
        onPress={this.handleSignout}>
        LOGOUT
      </Button>
      <View style={themedStyle.separator} />
      <Button style={themedStyle.button} appearance='ghost' status='danger'>
        MESSAGE
      </Button>
    </View>
    <Gallery items={images} />
  </View>
)

Here is the output of the above snippet:

Blank user profile page with Logout and Message buttons

Adding Logout functionality

You must have noticed in the previous section's snippet that there is an onPress attribute on a button with contents LOGOUT. It accepts a handler method as the value. Add the following handler method in your _Profile component.

handleSignout = async () => {
  try {
    await this.props.firebase.signOut()
    this.props.navigation.navigate('Auth')
  } catch (error) {
    console.log(error)
  }
}

To make it work, make sure that src/utils/Firebase.js profile has the following query method.

signOut: () => {
  return firebase.auth().signOut()
}

Here is the output:

Tapping the "Logout" button logs the user out

The images for each user's profile are shown using a FlatList component. The grid will have three images in a row; however, you can edit this to your liking. Create a new file Gallery.js inside the src/components/ directory and import the following statements.

import React, { Component } from 'react'
import {
  View,
  FlatList,
  Dimensions,
  StyleSheet,
  Image,
  TouchableOpacity
} from 'react-native'

The Gallery component receives a prop called items from the Profile component. This prop will have a similar data structure.

[
  {
    uri:
      'file:///Users/amanhimself/Library/Developer/CoreSimulator/Devices/8B7FC54D-3BA2-4679-89DC-062DDA882EFD/data/Containers/Data/Application/E3280502-CEA3-48E5-A390-9D9E4182D4E5/tmp/0B64032C-DB63-47E2-81C0-98344F172A7C.jpg'
  },
  {
    uri:
      'file:///Users/amanhimself/Library/Developer/CoreSimulator/Devices/8B7FC54D-3BA2-4679-89DC-062DDA882EFD/data/Containers/Data/Application/E3280502-CEA3-48E5-A390-9D9E4182D4E5/tmp/6E9E2631-AA76-4526-945C-98FBF31C6DEE.jpg'
  },
  {
    uri:
      'file:///Users/amanhimself/Library/Developer/CoreSimulator/Devices/8B7FC54D-3BA2-4679-89DC-062DDA882EFD/data/Containers/Data/Application/E3280502-CEA3-48E5-A390-9D9E4182D4E5/tmp/05AAAFD6-FB09-47EC-ACD3-7AB1F8562B69.jpg'
  }
]

Using Dimensions from the react-native API, the width of the screen is calculated and stored in itemSize such that three images are displayed in a row.

Here is the snippet for the Gallery component so far.

class Gallery extends Component {
  constructor(props) {
    super(props)
    const itemSize = (Dimensions.get('window').width - 12) / 3
    this.state = {
      data: this.props.items,
      itemSize,
      total: this.props.items.length
    }
  }

  //...
}

const styles = StyleSheet.create({
  images: {
    flexDirection: 'row',
    paddingHorizontal: 0.5
  },
  header: {
    flexDirection: 'row',
    justifyContent: 'space-between',
    backgroundColor: 'white'
  }
})

export default Gallery

Make sure you add the styles. Next, add the render() to display the images on the screen. This component uses FlatList to display images as you have seen in the Feed screen in the previous post.

FlatList requires three mandatory attributes:

  • data: an array of data
  • renderItem: that contains the JSX for each item in the data array
  • keyExtractor: used to extract a unique key for a given item at the specified index

Another attribute you are going to use is called numColumns. It accepts a number as its value and has the functionality to render multiple columns based on that value, and it automatically adds a flexWrap layout. The demo app is going to have three columns.

extractItemKey = index => `${index}`

 renderItem = ({ item, index }) => (
 <React.Fragment>
 <TouchableOpacity onPress={() => alert('add functionality to open')}>
 <Image
 style={{
 width: this.state.itemSize,
 height: this.state.itemSize,
 margin: 1.5
 }}
 source={item}
 />
 </TouchableOpacity>
 </React.Fragment>
 )

 render() {
 return (
 <View style={styles.images}>
 <FlatList
 data={this.state.data}
 numColumns={3}
 keyExtractor={this.extractItemKey}
 renderItem={this.renderItem}
 />
 </View>
 )
 }

Here is the output you will get on the emulator at the end of this section.

The app now has a photo grid with three images

Fetch user details

Let us write a new Firebase query to fetch user details and display them under the user profile. For example, it can display the correct user name under their profile avatar.

Open src/utils/Firebase.js and add the following query. This query, based on the current user's id, will fetch the right document that has the same uid in the collection users.

getUserDetails: () => {
  let user = firebase.auth().currentUser
  return firebase
    .firestore()
    .collection('users')
    .doc(user.uid)
    .get()
    .then(function(doc) {
      let userDetails = doc.data()
      return userDetails
    })
    .catch(function(error) {
      console.log('Error getting documents: ', error)
    })
}

Next, go back to the src/screens/Profile.js and update the state with a new userDetails object.

state = {
  images: [],
  // add this
  userDetails: {}
}

Next, add a new asynchronous method fetchUserDetails that will update the state object previously created.

fetchUserDetails = async () => {
  try {
    const userDetails = await this.props.firebase.getUserDetails()
    this.setState({ userDetails })
  } catch (error) {
    console.log(error)
  }
}

The results fetched by this asynchronous function are the complete details inside the Firestore document.

Lastly, make sure to invoke this function as soon as the component renders.

componentDidMount() {
 // add this
 this.fetchUserDetails()
 // ...
 this.fetchPosts()
 }

In the render method, instead of Test User, use the state variable userDetails.name to display the correct user name.

// first, update the destructuring
const { images, userDetails } = this.state

// next, add this
<Text category='h6' style={themedStyle.text}>
 {userDetails.name}
</Text>

Here is the output:

A user profile with user image

Add query for uploading an avatar image

In this section, you are going to add functionality to let users upload their own avatar image instead of showing a static image for every user. To begin, add the query function uploadAvatar to upload the image to the Firestore. This query function is going to have the URI to the image as its argument.

Open src/utils/Firebase.js and add the following:

uploadAvatar: avatarImage => {
  let user = firebase.auth().currentUser

  return firebase
    .firestore()
    .collection('users')
    .doc(user.uid)
    .update({
      avatar: avatarImage
    })
}

Add Edit Avatar Screen

Before you proceed, create a new screen component file EditAvatar.js in src/screens directory with some mock output that displays a text.

Open src/navigation/StackNavigator.js and let us add a stack navigation pattern for the profile screen. Since the edit avatar screen has only one entry point and that is the Profile screen, it is better to perform this step.

import Profile from '../screens/Profile'
import EditAvatar from '../screens/EditAvatar'

Now, export the Profile Navigator that has its own stack of two screens.

export const ProfileNavigator = createAppContainer(
  createStackNavigator({
    Profile: {
      screen: Profile
    },
    EditAvatar: {
      screen: EditAvatar
    }
  })
)

The main entry point for the Profile screen is through the bottom tab. You will have to let the TabNavigator know about this change. Open src/navigation/TabNavigator.js and import ProfileNavigator.

import { FeedNavigator, ProfileNavigator } from './StackNavigator'

Now, replace the value of screen in Profile.

Profile: {
 screen: ProfileNavigator,
 //... rest remains same
}

Add an edit icon

Open src/screens/Profile.js and add TouchableOpacity from react-native as well as Icon from UI kitten.

import { View, TouchableOpacity } from 'react-native'
import { Text, Button, withStyles, Avatar, Icon } from 'react-native-ui-kitten'

You are going to add a small edit icon to the user's avatar. Here is the output to be achieved by the end of this section.

An avatar with an edit button attached

To do so, first, modify the render method where the Avatar component is used from UI kitten and add the following in place of it.

The source attribute will now fetch the image from the userDetails. If the avatar image exists, it will be shown. The TouchableOpacity is used to let the user navigate to the EditAvatar screen.

<View>
  <Avatar
    source={this.state.userDetails.avatar}
    size='giant'
    style={{ width: 100, height: 100 }}
  />
  <View style={themedStyle.add}>
    <TouchableOpacity onPress={this.handleEditAvatarNavigation}>
      <Icon name='edit-outline' width={20} height={20} fill='#111' />
    </TouchableOpacity>
  </View>
</View>

Add the following handler method before render to perform the navigation between two screens.

handleEditAvatarNavigation = () => {
  this.props.navigation.navigate('EditAvatar')
}

Lastly, add the following styles to achieve accurate results. Using position: absolute the desired output can be achieved.

add: {
 backgroundColor: '#939393',
 position: 'absolute',
 bottom: 0,
 right: 0,
 width: 30,
 height: 30,
 borderRadius: 15,
 alignItems: 'center',
 justifyContent: 'center'
}

Add EditAvatar screen

Open src/screens/EditAvatar.js and import the following statements.

import React, { Component } from 'react'
import { View, Image } from 'react-native'
import { Button, Text } from 'react-native-ui-kitten'
import ImagePicker from 'react-native-image-picker'
import { withFirebaseHOC } from '../utils'

Using react-native-image-picker you can let the user select an image. This is the same process you followed while adding an image for the post.

Add the EditAvatar class component with the following code snippet.

class EditAvatar extends Component {
  state = {
    avatarImage: null
  }

  selectImage = () => {
    const options = {
      noData: true
    }
    ImagePicker.launchImageLibrary(options, response => {
      if (response.didCancel) {
        console.log('User cancelled image picker')
      } else if (response.error) {
        console.log('ImagePicker Error: ', response.error)
      } else if (response.customButton) {
        console.log('User tapped custom button: ', response.customButton)
      } else {
        const source = { uri: response.uri }
        this.setState({
          avatarImage: source
        })
      }
    })
  }

  onSubmit = async () => {
    try {
      const avatarImage = this.state.avatarImage
      this.props.firebase.uploadAvatar(avatarImage)

      this.setState({
        avatarImage: null
      })
    } catch (e) {
      console.error(e)
    }
  }

  render() {
    return (
      <View style={{ flex: 1, alignItems: 'center', justifyContent: 'center' }}>
        <Text category='h2'>Edit Avatar</Text>
        <View>
          {this.state.avatarImage ? (
            <Image
              source={this.state.avatarImage}
              style={{ width: 300, height: 300 }}
            />
          ) : (
            <Button
              onPress={this.selectImage}
              style={{
                alignItems: 'center',
                padding: 10,
                margin: 30
              }}>
              Add an image
            </Button>
          )}
        </View>
        <Button
          status='success'
          onPress={this.onSubmit}
          style={{ marginTop: 30 }}>
          Add post
        </Button>
      </View>
    )
  }
}

export default withFirebaseHOC(EditAvatar)

You will get the following output:

An avatar editor page with the option to upload an image or post

On clicking the Add an image button a new image can be selected.

An image has been selected

Click the button Add post and go back to the Profile screen by clicking the back button on the top left corner. The new user image is reflected on the screen as well as the Firestore.

Filestore renders the correct URI for the new image

Conclusion

The Profile screen implemented is from one of the templates from Crowdbotics' react-native collection. We use UI Kitten for our latest template libraries. You can modify the screen further or add another component that takes care of counting likes or comments. Find more about how to create custom screens like this from our open source project here.

You can also find the source code from this tutorial at this Github repo.