by

What We Will Be Building

Now that we know how to log users in using Facebook or Google OAuth let’s build a comments component. It’ll show existing comments, and allow users to submit new comments.

There’s going to be two main parts:

  1. Node.js backend. Authenticates users via Facebook or Google OAuth and stores users their comments in MongoDB database.
  2. React Native mobile app. Has Login and Comments screens. The former is to log users in, and the latter is to show user comments and to post new ones.

Let’s Get Started

Let’s start off by pulling the code from Logging Into React Native Apps with Facebook or Google tutorial to use it as a boilerplate and build on top of it.

git clone https://github.com/rationalappdev/react-native-oauth-login-tutorial.git Comments;
cd Comments;
rm -rf .git;

And then install dependencies.

npm install;

Backend

First, let’s modify our backend code so that it can handle user and comment creation using MongoDB.

MongoDB

If you don’t have MongoDB installed yet, open Terminal App and execute:

brew install mongodb

Then create a folder that MongoDB will use to store the database data:

sudo mkdir -p /data/db

And set the correct permissions:

sudo chmod 777 /data/db

Finally, launch MongoDB:

mongod&

Install Dependencies

  • Install existing dependencies.
cd backend;
npm install;
  • Install Mongoose and body-parser.
npm install mongoose body-parser --save;

Models

Now, let’s create Mongoose models for users and comments. We’ll use those to store users and comments in MongoDB.

  • Create a new folder called models within backed folder.

User Model

  • Create a new file called user.js within models folder.
import mongoose, { Schema } from 'mongoose';

// Define model schema
export const schema = new Schema({
  oauth_id: {
    type: String,
    unique: true,
    index: true,
  },
  name: String,
  avatar: String,
});

// Export Mongoose model
export default mongoose.model('User', schema);

Comment Model

  • Create a new file called comment.js within models folder.
import mongoose, { Schema } from 'mongoose';

// Define model schema
export const schema = new Schema({
  // References User model
  user: {
    type: Schema.ObjectId,
    ref: 'User',
  },
  content: String,
  created: Date
});

// Export Mongoose model
export default mongoose.model('Comment', schema);

Controllers

Currently, we have all of the authorization logic in one file server.js. But since our app is getting bigger and we’re adding comments it would be better to break the code down into different controllers to divide responsibilities. Generally, it’s a good idea to follow Single Responsibility Principle.

The single responsibility principle is a computer programming principle that states that every module or class should have responsibility over a single part of the functionality provided by the software, and that responsibility should be entirely encapsulated by the class. – Wikipedia.

  • Create a new folder called controllers within backed folder.

Auth Controller

Let’s move all authorization logic from server.js to auth.js controller.

  • Create a new file called auth.js within controllers folder.
import passport from 'passport';
import FacebookStrategy from 'passport-facebook';
import GoogleStrategy from 'passport-google-oauth20';
import User from '../models/user';
// Import Facebook and Google OAuth apps configs
import { facebook, google } from '../config';

// Transform Facebook profile because Facebook and Google profile objects look different
// and we want to transform them into user objects that have the same set of attributes
const transformFacebookProfile = (profile) => ({
  oauth_id: profile.id,
  name: profile.name,
  avatar: profile.picture.data.url,
});

// Transform Google profile into user object
const transformGoogleProfile = (profile) => ({
  oauth_id: profile.id,
  name: profile.displayName,
  avatar: profile.image.url,
});

// Register Facebook Passport strategy
passport.use(new FacebookStrategy(facebook,
  // Gets called when user authorizes access to their profile
  async (accessToken, refreshToken, profile, done)
    // Return done callback and pass transformed user object
    => done(null, await createOrGetUserFromDatabase(transformFacebookProfile(profile._json)))
));

// Register Google Passport strategy
passport.use(new GoogleStrategy(google,
  async (accessToken, refreshToken, profile, done)
    => done(null, await createOrGetUserFromDatabase(transformGoogleProfile(profile._json)))
));

const createOrGetUserFromDatabase = async (userProfile) => {
  let user = await User.findOne({ 'oauth_id': userProfile.oauth_id }).exec();
  if (!user) {
    user = new User({
      oauth_id: userProfile.oauth_id,
      name: userProfile.name,
      avatar: userProfile.avatar,
    });
    await user.save();
  }
  return user;
};

// Serialize user into the sessions
passport.serializeUser((user, done) => done(null, user));

// Deserialize user from the sessions
passport.deserializeUser((user, done) => done(null, user));

// Facebook
export const facebookLogin = passport.authenticate('facebook');
export const facebookMiddleware = passport.authenticate('facebook', { failureRedirect: '/auth/facebook' });

// Google
export const googleLogin = passport.authenticate('google', { scope: ['profile'] });
export const googleMiddleware = passport.authenticate('google', { failureRedirect: '/auth/google' });

// Callback
export const oauthCallback = async (req, res) => {
  res.redirect('OAuthLogin://login?user=' + JSON.stringify(req.user));
};

Comments Controller

Now, let’s create comments.js controller with two actions: list and create. list returns all existing comments from the database, and create adds new comments.

  • Create a new file called comments.js within controllers folder.
import Comment from '../models/comment';

// User relation for .populate()
const userRelation = {
  path: 'user',
  select: ['name', 'avatar'],
  model: 'User',
};

// List existing comments
export const list = async (req, res, next) => {
  // Get all comments and populate User models
  const comments = await Comment.find()
    .sort({ 'created': -1 })
    .populate(userRelation)
    .exec();

  res.json({
    comments
  });
};

// Create new comment
export const create = async (req, res, next) => {
  const { user_id, content } = req.body;
  // Save comment
  const comment = await new Comment({
    user: user_id,
    content: content,
    created: new Date,
  }).save();

  res.json({
    // Get the comment and populate User model
    comment: await Comment.findById(comment._id)
      .populate(userRelation)
      .exec()
  });
};

Server

In the next step let’s update server.js to import controller actions that we just created and register the routes.

  • Open server.js file and replace its content with the following.
import express from 'express';
import bodyParser from 'body-parser';
import passport from 'passport';
import mongoose from 'mongoose';
import {
  facebookLogin,
  facebookMiddleware,
  googleLogin,
  googleMiddleware,
  oauthCallback,
} from './controllers/auth';
import { list, create } from './controllers/comments';

// Connect to MongoDB
mongoose.Promise = global.Promise;
mongoose.connect('mongodb://localhost/react-native-comments');

// Initialize http server
const app = express();
app.use(bodyParser.json());
// Initialize Passport
app.use(passport.initialize());
app.use(passport.session());

// Set up auth routes
app.get('/auth/facebook', facebookLogin);
app.get('/auth/google', googleLogin);
app.get('/auth/facebook/callback', facebookMiddleware, oauthCallback);
app.get('/auth/google/callback', googleMiddleware, oauthCallback);

// Set up comment routes
app.route('/comments')
  .get(list)
  .put(create);

// Launch the server on the port 3000
const server = app.listen(3000, () => {
  const { address, port } = server.address();
  console.log(`Listening at http://${address}:${port}`);
});

Launch the Backend

We’re done making changes to the backend. Let’s launch it to make sure it still works.

  • Open Terminal App and execute:
npm start

You should see a confirmation that it’s running in your terminal.

Now, let’s open a browser and go to http://127.0.0.1:3000/comments.

There are no comments yet, but the server works, which is great. Keep it running and let’s work on the mobile app.

Mobile App

Now that we’re done with the backend let’s go ahead and make use of it and build a mobile app!

Install Dependencies

  • Install existing dependencies.
cd Comments;
npm install;
  • Install moment that’ll help us out with formatting date times in comments.
npm install moment --save;

Login Component

Since, again, our app is getting bigger and more complicated let’s break everything down into different components. And start off by moving all of the login logic from app.js to its own component called Login.

  • Create a new folder called components.
  • Create a new file called login.js within components folder.
import React, { Component, PropTypes } from 'react';
import {
  Linking,
  StyleSheet,
  Platform,
  Text,
  View
} from 'react-native';
import Icon from 'react-native-vector-icons/FontAwesome';
import SafariView from 'react-native-safari-view';

export default class Login extends Component {

  static propTypes = {
    onLoggedIn: PropTypes.func.isRequired
  };

  // Set up Linking
  componentDidMount() {
    // Add event listener to handle OAuthLogin:// URLs
    Linking.addEventListener('url', this.handleOpenURL);
    // Launched from an external URL
    Linking.getInitialURL().then((url) => {
      if (url) {
        this.handleOpenURL({ url });
      }
    });
  };

  componentWillUnmount() {
    // Remove event listener
    Linking.removeEventListener('url', this.handleOpenURL);
  };

  handleOpenURL = ({ url }) => {
    // Extract stringified user string out of the URL
    const [, user_string] = url.match(/user=([^#]+)/);
    // Decode the user string and parse it into JSON
    const user = JSON.parse(decodeURI(user_string));
    // Call onLoggedIn function of parent component and pass user object
    this.props.onLoggedIn(user);
    if (Platform.OS === 'ios') {
      SafariView.dismiss();
    }
  };

  // Handle Login with Facebook button tap
  loginWithFacebook = () => this.openURL('http://localhost:3000/auth/facebook');

  // Handle Login with Google button tap
  loginWithGoogle = () => this.openURL('http://localhost:3000/auth/google');

  // Open URL in a browser
  openURL = (url) => {
    // Use SafariView on iOS
    if (Platform.OS === 'ios') {
      SafariView.show({
        url: url,
        fromBottom: true,
      });
    }
    // Or Linking.openURL on Android
    else {
      Linking.openURL(url);
    }
  };

  render() {
    return (
      <View style={styles.container}>

        <View style={styles.content}>
          <Text style={styles.header}>
            Welcome Stranger!
          </Text>
          <View style={styles.avatar}>
            <Icon name="user-circle" size={100} color="rgba(0,0,0,.09)" />
          </View>
          <Text style={styles.text}>
            Please log in to continue {'\n'}
            to the awesomness
          </Text>
        </View>

        <View style={styles.buttons}>
          <Icon.Button
            name="facebook"
            backgroundColor="#3b5998"
            onPress={this.loginWithFacebook}
            {...iconStyles}
          >
            Login with Facebook
          </Icon.Button>
          <Icon.Button
            name="google"
            backgroundColor="#DD4B39"
            onPress={this.loginWithGoogle}
            {...iconStyles}
          >
            Or with Google
          </Icon.Button>
        </View>

      </View>
    );
  }
}

const iconStyles = {
  borderRadius: 10,
  iconStyle: { paddingVertical: 5 },
};

const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: '#FFF',
  },
  content: {
    flex: 1,
    justifyContent: 'center',
    alignItems: 'center',
  },
  avatar: {
    margin: 20,
  },
  avatarImage: {
    borderRadius: 50,
    height: 100,
    width: 100,
  },
  header: {
    fontSize: 20,
    textAlign: 'center',
    margin: 10,
  },
  text: {
    textAlign: 'center',
    color: '#333',
    marginBottom: 5,
  },
  buttons: {
    justifyContent: 'space-between',
    flexDirection: 'row',
    margin: 20,
    marginBottom: 30,
  },
});

API Helpers

Before we get started building comment components, let’s create a couple of helper functions that will help us make API calls.

  • Create a new file called api.js within project root folder.
// Our API backend's URL
const API = 'http://localhost:3000';

const headers = {
  'Accept': 'application/json',
  'Content-Type': 'application/json',
  'X-Requested-With': 'XMLHttpRequest'
};

export const get = async (uri) => await fetch(`${API}/${uri}`, {
  method: 'GET',
  headers,
});

export const put = async (uri, body) => await fetch(`${API}/${uri}`, {
  method: 'PUT',
  body: JSON.stringify(body),
  headers,
});

For now, we just need to make GET and PUT calls to fetch or post new comments. If you decided to add a comment deletion feature, you could easily add new delete function.

Comments Components

For comments we’re going to have three different components:

  1. List. Renders a scrollable list of comments and an input field for posting new comments using Comment and Input components respectively.
  2. Comment. Renders a single comment with an avatar, user name, content and how long ago it was posted.
  3. Input. Renders an input field for posting new comments.
  • Create a new folder called comments within components folder.

Comment Component

First, let’s create Comment component that renders each component.

  • Create a new file called comment.js within comments folder.
import React, { PureComponent, PropTypes } from 'react';
import {
  Image,
  StyleSheet,
  Text,
  View
} from 'react-native';
import moment from 'moment';

export default class Comment extends PureComponent {

  static propTypes = {
    // Comment object shape
    comment: PropTypes.shape({
      content: PropTypes.string.isRequired,
      created: PropTypes.string.isRequired,
      // User object shape
      user: PropTypes.shape({
        name: PropTypes.string.isRequired,
        avatar: PropTypes.string.isRequired,
      }).isRequired,
    }).isRequired,
  };

  render() {
    // Pull comment object out of props
    const { comment } = this.props;
    // Pull data needed to display a comment out of comment object
    const { content, created, user } = comment;
    // Pull user name and avatar out of user object
    const { name, avatar } = user;
    return (
      <View style={styles.container}>
        <View style={styles.avatarContainer}>
          {avatar && <Image
            resizeMode='contain'
            style={styles.avatar}
            source={{ uri: avatar }}
          />}
        </View>
        <View style={styles.contentContainer}>
          <Text>
            <Text style={[styles.text, styles.name]}>{name}</Text>
            {' '}
            <Text style={styles.text}>{content}</Text>
          </Text>
          <Text style={[styles.text, styles.created]}>{moment(created).fromNow()}</Text>
        </View>
      </View>
    );
  }

}

const styles = StyleSheet.create({
  container: {
    flexDirection: 'row',
  },
  avatarContainer: {
    alignItems: 'center',
    marginLeft: 5,
    paddingTop: 10,
    width: 40,
  },
  contentContainer: {
    flex: 1,
    borderBottomWidth: 1,
    borderColor: '#EEE',
    padding: 5,
  },
  avatar: {
    borderWidth: 1,
    borderColor: '#EEE',
    borderRadius: 13,
    width: 26,
    height: 26,
  },
  text: {
    color: '#000',
    fontFamily: 'Avenir',
    fontSize: 15,
  },
  name: {
    fontWeight: 'bold',
  },
  created: {
    color: '#BBB',
  },
});

It’s a pretty basic component that takes in comment object as a prop and renders user’s avatar, name, their comment and how long ago it was posted.

Input Component

Next, let’s build Input component that allows users to post new comments.

  • Create a new file called input.js within comments folder.
import React, { Component, PropTypes } from 'react';
import {
  KeyboardAvoidingView,
  StyleSheet,
  TextInput,
  Text,
  View, TouchableOpacity
} from 'react-native';

export default class Input extends Component {

  static propTypes = {
    onSubmit: PropTypes.func.isRequired,
  };

  state = {
    text: undefined, // user's input
  };

  // Update state when input changes
  onChangeText = (text) => this.setState({ text });

  // Handle return press on the keyboard
  // NOTE: You don't really need it for this example, because
  // we're using a keyboard without return button, but I left it here
  // in case you'd want to switch to a different keyboard
  onSubmitEditing = ({ nativeEvent: { text } }) => this.setState({ text }, this.submit);

  // Call this.props.onSubmit handler and pass the comment
  submit = () => {
    const { text } = this.state;
    if (text) {
      this.setState({ text: undefined }, () => this.props.onSubmit(text));
    } else {
      alert('Please enter your comment first');
    }
  };

  render() {
    return (
      // This moves children view with input field and submit button
      // up above the keyboard when it's active
      <KeyboardAvoidingView
        behavior='position'
      >
        <View style={styles.container}>
          {/* Comment input field */}
          <TextInput
            placeholder="Add a comment..."
            keyboardType="twitter" // keyboard with no return button
            autoFocus={true} // focus and show the keyboard
            style={styles.input}
            value={this.state.text}
            onChangeText={this.onChangeText} // handle input changes
            onSubmitEditing={this.onSubmitEditing} // handle submit event
          />
          {/* Post button */}
          <TouchableOpacity
            style={styles.button}
            onPress={this.submit}
          >
            {/* Apply inactive style if no input */}
            <Text style={[styles.text, !this.state.text ? styles.inactive : []]}>Post</Text>
          </TouchableOpacity>
        </View>
      </KeyboardAvoidingView>
    );
  }

}

const styles = StyleSheet.create({
  container: {
    backgroundColor: '#FFF',
    flexDirection: 'row',
    borderTopWidth: 1,
    borderColor: '#EEE',
    alignItems: 'center',
    paddingLeft: 15,
  },
  input: {
    flex: 1,
    height: 40,
    fontSize: 15,
  },
  button: {
    height: 40,
    paddingHorizontal: 20,
    alignItems: 'center',
    justifyContent: 'center',
  },
  inactive: {
    color: '#CCC',
  },
  text: {
    color: '#3F51B5',
    fontWeight: 'bold',
    fontFamily: 'Avenir',
    textAlign: 'center',
    fontSize: 15,
  },
});

This component has an input field that takes users input and passes that input to onSubmit function passed as a prop when they press Post button.

List Component

Finally, let’s make use of two components we just build in previous steps.

  • Create a new file called list.js within comments folder.
import React, { Component, PropTypes } from 'react';
import {
  RefreshControl,
  ScrollView,
  StyleSheet,
  View
} from 'react-native';
import { get, put } from '../../api';
import Comment from './comment';
import Input from './input';

export default class List extends Component {

  static propTypes = {
    // User object shape
    user: PropTypes.shape({
      _id: PropTypes.string.isRequired,
    }).isRequired,
  };

  state = {
    comments: [], // array for comments fetched from the API backend
    refreshing: true, // whether comments list is being refreshed or not
  };

  // Fetch comments when component is about to mount
  componentWillMount = () => this.fetchComments();

  // Re-fetch comments when user pulls the list down
  onRefresh = () => this.fetchComments();

  // Call API to fetch comments
  fetchComments = async () => {
    this.setState({ refreshing: true });
    try {
      // Make API call
      const response = await get('comments');
      // Convert response to JSON
      const json = await response.json();
      this.setState({
        refreshing: false,
        comments: json.comments
      });
    }
    catch (error) {
      alert(error);
    }
  };

  // Call API to submit a new comment
  submitComment = async (comment) => {
    const { user } = this.props;
    this._scrollView.scrollTo({ y: 0 });
    try {
      // Make API call
      const response = await put('comments', {
        user_id: user._id,
        content: comment,
      });
      // Convert response to JSON
      const json = await response.json();
      this.setState({
        // Push new comment to state before existing ones
        comments: [json.comment, ...this.state.comments]
      });
    }
    catch (error) {
      alert(error);
    }
  };

  render() {
    // Pull comments out of state
    const { comments } = this.state;
    return (
      <View style={styles.container}>
        {/* Scrollable list */}
        <ScrollView
          ref={(scrollView) => { this._scrollView = scrollView; }}
          refreshControl={
            <RefreshControl
              refreshing={this.state.refreshing}
              onRefresh={this.onRefresh}
            />
          }
        >
          {/* Render each comment with Comment component */}
          {comments.map((comment, index) => <Comment comment={comment} key={index} />)}
        </ScrollView>
        {/* Comment input box */}
        <Input onSubmit={this.submitComment} />
      </View>
    );
  }

}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: '#FFF',
    paddingTop: 20,
  }
});

This component makes an API call to fetch existing comments when being loaded for the first time. Then it renders a scrollable list with comments rendered as Comment components and an input field rendered by Input component. It also allows users to refresh comments by pulling down the list.

App Component

And in the final step let’s update App component to use our new Login and Comments components.

  • Open app.js within project root folder.
import React, { Component } from 'react';
import Login from './components/login';
import Comments from './components/comments/list';

export default class App extends Component {

  state = {
    user: undefined, // not logged in yet
  };

  // Gets called after user logs in with Facebook or Google
  onLoggedIn = (user) => {
    this.setState({ user });
  };

  render() {
    const { user } = this.state;
    return user
      // Show comments if user is logged in
      ? <Comments user={user} />
      // Show login screen otherwise
      : <Login onLoggedIn={this.onLoggedIn} />;
  }
}

All Set

And we’re all done. Let’s launch our app to make sure that everything is working as expected.

  1. Make sure our Node.js backend is still up and running.
  2. Launch the mobile app by executing react-native run-ios in the terminal.

Wrapping Up

Hopefully, you’ve learned a lot and will be able to use that knowledge when building your apps! Subscribe to get notified about new tutorials. And if you have any questions or ideas for new tutorials, just leave a comment below the post.

Spread the Word

Don't miss out on new tutorials

  • iwebworld info

    this site providing good information about NodeJS, To learn NodeJS visit http://iwebworld.info contact: [email protected]

  • Juan felipe Ortega Lugo

    This is a good stuff, but when i used this in ios doesn’t works the KeyboardAvoidingView, dont u
    know what happend with this? thanks!