Chapter 12: Authentication

Full Stack React Native

link Authentication

By the end of this lesson, developers will be able to:

  • Create a GraphQL client with built-in authentication
  • Create and Destroy votes

link The Story So Far

So far, we set up a React Native application and an Express server. The app is designed to allow users to sign up, login and create votes.

App Setup

git clone https://github.com/Maelstroms38/movies-starter.git
cd movies-starter
yarn
yarn start

Server Setup

git clone https://github.com/Maelstroms38/movies-api
cd movies-api
yarn
yarn db:reset
yarn start

Setting Up

Let's start by opening and running both projects in two separate windows.

cd <app-name>
yarn start

cd <server-name>
yarn start

Once you have confirmed both projects are running, we will add a client to the React Native project.

Heads Up! This lesson's code edits will only affect your React Native project.

link Apollo Link

Unless all of the data you are loading is completely public, your app has some sort of users, accounts and permissions systems. If different users have different permissions in your application, then you need a way to tell the server which user is associated with each request.

Apollo Client uses the ultra flexible Apollo Link that includes several options for authentication.

There are a number of useful links that have already been implemented that may be useful for your application.

Available Links

The setup for this project deviates from previous React Native clients. In this case, we will be using the apollo-link-http dependency:

Let's install the dependencies we need to get started:

yarn add @apollo/react-hooks apollo-client apollo-cache-inmemory apollo-link apollo-link-http apollo-utilities graphql graphql-tag

Make the following changes in the top level imports of App.js:

import React from 'react';
import { StyleSheet, Text, View, AsyncStorage } from 'react-native';
import { split } from 'apollo-link';
import AppNavigator from './AppNavigator';
import { ApolloClient } from 'apollo-client';
import { ApolloProvider } from '@apollo/react-hooks';
import { InMemoryCache } from 'apollo-cache-inmemory';
import { HttpLink } from 'apollo-link-http';
import { ApolloLink, Observable } from 'apollo-link';


const BASE_URL = 'http://localhost:4000';

It's time to setup the GraphQL auth client. The client will determine whether to send a authorization header with its authentication token.

Apollo Client has a pluggable network interface layer, which can let you configure how queries are sent over HTTP, or replace the whole network part with something completely custom, like a websocket transport, mocked server data, or anything else you can imagine.

Using a link

To create a link to use with Apollo Client, you can install and import one from npm or create your own. We recommend using apollo-link-http for most setups.

Here's how you would instantiate a new client with a custom endpoint URL using the HttpLink:

// https://www.apollographql.com/docs/react/migrating/boost-migration/
const httpLink = new HttpLink({
  uri: BASE_URL,
  credentials: 'include',
});

const request = async (operation) => {
  const token = await AsyncStorage.getItem('token');
  operation.setContext({
    headers: {
      authorization: token ? `Bearer ${token}` : ''
    }
  });
};

const requestLink = new ApolloLink((operation, forward) =>
  new Observable(observer => {
    let handle;
    Promise.resolve(operation)
      .then(oper => request(oper))
      .then(() => {
        handle = forward(operation).subscribe({
          next: observer.next.bind(observer),
          error: observer.error.bind(observer),
          complete: observer.complete.bind(observer),
        });
      })
      .catch(observer.error.bind(observer));

    return () => {
      if (handle) handle.unsubscribe();
    };
  })
);

const client = new ApolloClient({
  link: ApolloLink.from([requestLink, httpLink]),
  cache: new InMemoryCache(),
});

Finally, wrap the AppNavigator inside the ApolloProvider:

export default function App() {
  return (
    <ApolloProvider client={client}>
      <View style={styles.container}>
        <AppNavigator />
      </View>
    </ApolloProvider>
  );
}

We now have a GraphQL client that will include an auth token from AsyncStorage. Let's utilize this functionality with an authentication flow. But before diving into auth, we will set up movie data queries.

It's easy to add an authorization header to every HTTP request by chaining together Apollo Links. In this example, we pull the token from AsyncStorage, every time a request is sent.

Click here to learn more about AsyncStorage.

link Feed Queries

The HomeScreen displays a static list of movies. Let's modify it to fetch the latest movies.

Make the following changes to the screens/HomeScreen.js top level imports:

- import React from 'react';
+ import React, { useState } from 'react';
import {
  Image,
  Platform,
  ScrollView,
  FlatList,
  StyleSheet,
  Text,
  TouchableOpacity,
  View,
  ActivityIndicator
} from 'react-native';
import MoviePoster from '../components/MoviePoster';
+ import { useQuery } from '@apollo/react-hooks';
+ import gql from 'graphql-tag'; 

-const movies = [
-  {
-    "title": "Spider-Man: Far From Home",
-    "description": "Peter Parker and his friends go on a summer trip to Europe. However, they will hardly be able to rest - Peter will have to agree to help Nick Fury uncover the mystery of creatures that cause natural disasters and destruction throughout the continent.",
-    "imageUrl": "https://image.tmdb.org/t/p/w780/lcq8dVxeeOqHvvgcte707K0KVx5.jpg",
-    "category": {
-      "title": "Action"
-  },
-  {
-    "title": "The Lion King",
-    "description": "Simba idolizes his father, King Mufasa, and takes to heart his own royal destiny. But not everyone in the kingdom celebrates the new cub's arrival. Scar, Mufasa's brother and a former heir to the throne has plans of his own.",
-    "imageUrl": "https://image.tmdb.org/t/p/w780/2bXbqYdUdNVa8VIWXVfclP2ICtT.jpg",
-    "category": {
-      "title": "Animation"
-    }
-  },
-  {
-    "title": "Ford v Ferrari",
-    "description": "American car designer Carroll Shelby and the British-born driver Ken Miles work together to battle corporate interference, the laws of physics, and their own personal demons to build a revolutionary race car for Ford Motor Company.",
-    "imageUrl": "https://image.tmdb.org/t/p/w780/6ApDtO7xaWAfPqfi2IARXIzj8QS.jpg",
-    "category": {
-      "title": "Thriller"
-    }
-  },
-  {
-    "title": "Once Upon a Time... in Hollywood",
-    "description": "A faded television actor and his stunt double strive to achieve fame and success in the film industry during the final years of Hollywood's Golden Age in 1969 Los Angeles.",
-    "imageUrl": "https://image.tmdb.org/t/p/w780/8j58iEBw9pOXFD2L0nt0ZXeHviB.jpg",
-    "category": {
-      "title": "Western"
-    }
-  },
-  {
-    "title": "Frozen II",
-    "description": "Elsa, Anna, Kristoff and Olaf are going far in the forest to know the truth about an ancient mystery of their kingdom..",
-    "imageUrl": "https://image.tmdb.org/t/p/w780/qdfARIhgpgZOBh3vfNhWS4hmSo3.jpg",
-    "category": {
-      "title": "Animation"
-    }
-  },
-  {
-    "title": "Joker",
-    "description": "During the 1980s, a failed stand-up comedian is driven insane and turns to a life of crime and chaos in Gotham City while becoming an infamous psychopathic crime figure.",
-    "imageUrl": "https://image.tmdb.org/t/p/w780/udDclJoHjfjb8Ekgsd4FDteOkCU.jpg",
-    "category": {
-      "title": "Horror"
-    }
-  },
-  {
-    "title": "Aladdin",
-    "description": "A kindhearted street urchin named Aladdin embarks on a magical adventure after finding a lamp that releases a wisecracking genie while a power-hungry Grand Vizier vies for the same lamp that has the power to make their deepest wishes come true.",
-    "imageUrl": "https://image.tmdb.org/t/p/w780/3iYQTLGoy7QnjcUYRJy4YrAgGvp.jpg",
-    "category": {
-      "title": "Animation"
-    }
-  }
- ];

Be sure to remove the static movies.

Add the following queries below the top level imports:

// Queries
const FEED_QUERY = gql`
  query Feed($categoryId: ID) {
    feed(categoryId: $categoryId) {
      id
      title
      description
      imageUrl
      category {
        title
      }
    }
  }
`;

const CATEGORY_QUERY = gql`
  query {
    categories {
      id
      title
    }
  }
`;

link Feed Hooks

We will query for movies and categories, using the useQuery method to fetch both entities. We will also enable useState to modify the current movie category.

export default function HomeScreen({ route, navigation }) {
  const [categoryId, setCategoryId] = useState(0);
  const { data, refetch, error } = useQuery(
    FEED_QUERY,
    {
      variables: categoryId ? {categoryId} : {},
      fetchPolicy: "cache-and-network"
    }
  );
  const { data: genres } = useQuery(CATEGORY_QUERY);

  if (error) {
    return <Text>{error.message}</Text>
  }
  if (!data || !data.feed) {
    return <ActivityIndicator color="#161616" style={{...StyleSheet.absoluteFillObject}} />
  }
  return (
    <View style={styles.container}>
      {genres ? <FlatList
          data={genres.categories}
          horizontal
          keyExtractor={(item, index) => {
            return `${index}`;
          }}
          extraData={categoryId}
          style={styles.bottomBorder}
          showsHorizontalScrollIndicator={false}
          renderItem={({ item, index }) => {
            const selected = categoryId == item.id;
            return (
              <Text>{item.title}</Text>
            )}} 
          />
        : null}
      <FlatList
        data={data.feed}
        keyExtractor={(item, index) => {
          return `${index}`;
        }}
        numColumns={2}
        decelerationRate="fast"
        renderItem={({ item, index }) => {
          return (
            <MoviePoster movie={item} onPress={() => navigation.navigate('Movie', {movie})} />
          )}} 
        />
    </View>
  );
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: '#fff',
  },
  bottomBorder: {
    borderBottomColor: "#d3d3d3", 
    borderBottomWidth: StyleSheet.hairlineWidth
  }
});

These changes provide a list of movies from the server. But we seem to be missing styling in the top section categories.

link Tag Component

Let's create a new Tag component to display category titles.

Create a new components/Tag.js file with the following contents:

import React from 'react';
import {
  Text,
  TouchableOpacity,
  View,
  StyleSheet
} from 'react-native';

export default function Tag(props) {
  const { title, onPress, selected } = props;
  return (
      <View
        style={styles.container}
      >
        <TouchableOpacity
          style={[styles.tag, {backgroundColor: selected ? "rgba(75, 148, 214, 1)" : "#FFF"}]}
          onPress={onPress}
        >
          <Text style={[styles.title, {color: selected ? "#FFF" : "#161616"}]}>{title}</Text>
        </TouchableOpacity>
      </View>
    );
}

const styles = StyleSheet.create({
  container: {
    margin: 6,
    height: 30,
  },
  tag: {
    flexDirection: 'row',
    alignItems: 'center',
    borderColor: 'gray',
    borderRadius: 20,
    borderWidth: StyleSheet.hairlineWidth,
    paddingHorizontal: 10,
    paddingVertical: 3,
  },
  title: {
    color: '#161616',
    fontSize: 18,
    fontWeight: 'normal',
  },
});

To import the Tag component, make the following changes inside the screens/HomeScreen.js top level imports:

import MoviePoster from '../components/MoviePoster';
+ import Tag from '../components/Tag';
import { useQuery } from '@apollo/react-hooks';
import gql from 'graphql-tag'; 

To make use of the new Tag component, make the following changes inside the screens/HomeScreen.js:

return (
    <View style={styles.container}>
      {genres ? <FlatList
          data={genres.categories}
          horizontal
          keyExtractor={(item, index) => {
            return `${index}`;
          }}
          extraData={categoryId}
          style={styles.bottomBorder}
          showsHorizontalScrollIndicator={false}
          renderItem={({ item, index }) => {
            const selected = categoryId == item.id;
            return (
-              <Text>{item.title}</Text>
+              <Tag key={index}
+                  selected={selected}
+                  title={item.title}
+                  onPress={() => {
+                      if (selected) {
+                        setCategoryId(0)
+                        refetch()
+                      } else {
+                        setCategoryId(parseInt(item.id))
+                        refetch()
+                      }
+                    }
+                  }
+                />
              )}} 
          />
        : null}

Each time a user selects any genre of film, they filter their movie list based on the selection. They may also deselect the specified genre by tapping a selected genre.

link Authentication Flow

Authentication is a critical feature for any modern mobile or web application. We require auth to verify users who can make changes to specific entities. In this case we give authorized users the ability to vote for movies.

Let's begin authentication with signing in and signing up for the app.

link LoginScreen

The LoginScreen allows users to enter their email address, username and password to sign in or sign up. Create a new file called screens/LoginScreen.js, and add the following:

import React, { useState } from 'react';
import { View, Text, StyleSheet, Dimensions, AsyncStorage, Alert, TextInput } from 'react-native';
import RoundedButton from '../components/RoundedButton';
import { useMutation } from '@apollo/react-hooks';
import { Ionicons } from '@expo/vector-icons';
import gql from 'graphql-tag';

const {width} = Dimensions.get('window');

// Mutations
const SIGNUP_MUTATION = gql`
  mutation SignUp($username:String!, $email:String!, $password:String!) {
    signUp(email:$email, username:$username,password:$password) {
      user {
        id
        username
        email
      }
      token
    }
  }
`;
const SIGNIN_MUTATION = gql`
  mutation SignIn($username:String, $email:String, $password:String!) {
    signIn(email:$email, username:$username,password:$password) {
      user {
        id
        username
        email
      }
      token
    }
  }
`;

export default function LoginScreen({ navigation }) {
  const [username, setUsername] = useState('');
  const [password, setPassword] = useState('');
  const [email, setEmail] = useState('');
  const [login, setLogin] = useState(false);

  // Signing In 
  const [signIn] = useMutation(SIGNIN_MUTATION, {
    async onCompleted({ signIn }) {
      const {token} = signIn;
      try {
        await AsyncStorage.setItem('token', token);
        navigation.replace('Profile');
      } catch(err) {
        console.log(err.message);
      }
    }
  });

  // Signing Up 
  const [signUp, { data: signedUp }] = useMutation(SIGNUP_MUTATION, {
    async onCompleted({ signUp }) {
      const {token} = signUp;
      try {
        await AsyncStorage.setItem('token', token);
        navigation.replace('Profile');
      } catch(err) {
        console.log(err.message);
      }
    }
  });

  return (
    <View style={styles.container}>
      <View style={styles.inputContainer}>
        {login ? null
         : <View>
          <Text>Username</Text>
          <TextInput 
              onChangeText={text => setUsername(text)}
              value={username}
              placeholder="Username"
              autoCorrect={false}
              autoCapitalize="none"
              style={styles.input}
            />
          </View>}
        <View>
          <Text>{login ? "Email or Username" : "Email"}</Text>
          <TextInput 
            onChangeText={text => setEmail(text)}
            value={email}
            placeholder={login ? "Email or Username" : "Email"}
            autoCorrect={false}
            autoCapitalize="none"
            style={styles.input}
          />
        </View>
        <View>
          <Text>Password</Text>
          <TextInput 
            onChangeText={text => setPassword(text)}
            value={password}
            placeholder="Password"
            autoCorrect={false}
            autoCapitalize="none"
            style={styles.input}
            secureTextEntry
          />
        </View>
      </View>

      <RoundedButton
        text={login ? "Login" : "Sign Up"}
        textColor="#fff"
        backgroundColor="rgba(75, 148, 214, 1)"
        onPress={() => {
          // TextInput validation
          let nullValues = [];
          if (!email) {
            nullValues.push("Email");
          }
          if (!username && !login) {
            nullValues.push("Username");
          }
          if (!password) {
            nullValues.push("Password");
          }
          if (nullValues.length) {
            Alert.alert(`Please fill in ${nullValues.join(', ')}`);
          } else {
            if (login) {
              // email validation
              const isEmail = email.includes('@');
              const res = isEmail ? signIn({ variables: { email, password } }) :
                    signIn({ variables: { username: email, password } });
            } else {
              signUp({ variables: { email, username, password } })
            }
          }
        }}
        icon={<Ionicons name="md-checkmark-circle" size={20} color={"#fff"} style={styles.saveIcon} />}
      />
      <RoundedButton
        text={login ? "Need an account? Sign Up" : "Have an account? Login"}
        textColor="rgba(75, 148, 214, 1)"
        backgroundColor="#fff"
        onPress={() => {
          setLogin(!login);
        }}
        icon={<Ionicons name="md-information-circle" size={20} color="rgba(75, 148, 214, 1)" style={styles.saveIcon} />}
         />
    </View>
  );
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    justifyContent: 'flex-start',
    alignItems: 'center',
    paddingHorizontal: 15,
  },
  saveIcon: {
    position: 'relative',
    left: 20,
    zIndex: 8
  },
  inputContainer: {
    flex: 0.40,
    justifyContent: 'space-around',
  },
  input: {
    width: width - 40,
    height: 40,
    borderBottomColor: '#FFF',
    borderBottomWidth: 1,
  },
});

Note that users can switch between sign in and sign up with a single form. To see the form in action, we will add it to the app's navigation tabs.

Add the following inside navigation/BottomNavigation.js:

import React from 'react';
import { createStackNavigator } from '@react-navigation/stack';

import HomeScreen from '../screens/HomeScreen';
import LoginScreen from '../screens/LoginScreen';

const Stack = createStackNavigator();

function HomeStack() {
  return (
    <Stack.Navigator>
      <Stack.Screen 
        name="Home" 
        component={HomeScreen} 
      />
    </Stack.Navigator>
  )
};

function ProfileStack() {
  return (
    <Stack.Navigator>
      <Stack.Screen
        name="Login" 
        component={LoginScreen} 
      />
    </Stack.Navigator>
  )
};

{/* ... */}

<BottomTab.Navigator initialRouteName={INITIAL_ROUTE_NAME}>
  <BottomTab.Screen
    name="Home"
    component={HomeStack}
    options={{
      title: 'Now Playing',
      tabBarIcon: ({ focused }) => <TabBarIcon focused={focused} name="md-code-working" />,
    }}
  />
  <BottomTab.Screen
    name="Detail"
    component={ProfileStack}
    options={{
      title: 'Welcome',
      tabBarIcon: ({ focused }) => <TabBarIcon focused={focused} name="md-person" />,
    }}
  />
</BottomTab.Navigator>

When a user logs in, they are taken to a new screen called Profile. Let's add a ProfileScreen and then an AuthLoading screen.

link ProfileScreen

Create a new screens/ProfileScreen.js, and add the following contents:

import React, { useEffect } from 'react';
import { View, Text, FlatList, StyleSheet, AsyncStorage, ActivityIndicator } from 'react-native';
import MoviePoster from '../components/MoviePoster';
import { Ionicons } from '@expo/vector-icons';
import { useQuery } from '@apollo/react-hooks';
// import Profile from '../components/Profile';
import gql from 'graphql-tag'; 

const PROFILE_QUERY = gql`
  query {
    currentUser {
      id
      username
      email
      votes {
        movie {
          id
          title
          description
          imageUrl
          category {
            title
          }
        }
      }
    }
  }
`;

export default function ProfileScreen({ route, navigation }) {
  const { data, loading, error } = useQuery(
    PROFILE_QUERY, 
    { fetchPolicy: "network-only" }
  );
  if (!data || !data.currentUser) {
    return <ActivityIndicator color="#161616" style={{...StyleSheet.absoluteFillObject}} />
  }
  const { currentUser } = data;
  const { username, email, votes } = currentUser;
  return (
    <View style={styles.container}>
      {/* <Profile currentUser={currentUser} /> */}
      {votes && votes.length ? <FlatList
        data={votes}
        keyExtractor={(item, index) => {
          return `${index}`;
        }}
        numColumns={2}
        decelerationRate="fast"
        renderItem={({ item, index }) => {
          const {movie} = item;
          return (
            <MoviePoster movie={movie} onPress={() => navigation.navigate('Movie', {movie})} />
          )}} 
        />
      : null}
    </View>
  );
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    paddingTop: 15,
    backgroundColor: '#fff',
  },
  saveIcon: {
    position: 'relative',
    right: 20,
    zIndex: 8
  },
});

Add the new ProfileScreen to navigation/BottomTabNavigator:

// navigation/BottomTabNavigator.js

import { AsyncStorage } from 'react-native';
import ProfileScreen from '../screens/ProfileScreen';
import MovieDetail from '../screens/MovieDetail';

/* ... */

function ProfileStack() {
  return (
    <Stack.Navigator>
      <Stack.Screen
        name="Login" 
        component={LoginScreen} 
      />
      <Stack.Screen
        name="Profile" 
        component={ProfileScreen} 
        options={({ navigation, route }) => ({
          headerRight: props => (
          <Ionicons 
            onPress={async () => {
                await AsyncStorage.removeItem('token');
                navigation.replace('Login')
              } 
            }
            name="md-exit" 
            size={25} 
            color={"#161616"} 
            style={{
              position: 'relative',
              left: 20,
              zIndex: 8
            }} />
          ),
        })} />
      />
    </Stack.Navigator>
  )
};

By now, you should be able to login and see the list of voted movies. We are missing a small component called Profile, to display our current user's info.

link Profile Component

Create a new components/Profile.js file that will display the current user's username, email, and their number of votes.

import React, { useState } from 'react';
import {
  Dimensions,
  StyleSheet,
  Text,
  TouchableOpacity,
  View
} from 'react-native';

export default function Profile(props) {

  const { currentUser } = props;
  const {
    username,
    email,
    votes,
  } = currentUser;

  return (
    <View style={styles.container}>
      <View style={styles.row}>
        <Text style={styles.text} numberOfLines={1}>
          {username}
        </Text>
        <View style={styles.right}>
          <Text style={styles.text} numberOfLines={1}>
            {votes.length} Vote(s)
          </Text>
        </View>
      </View>

      <View style={styles.row}>
        <Text style={[styles.text, styles.name]} numberOfLines={1}>
          {email}
        </Text>
      </View>
    </View>
  );

}

const styles = StyleSheet.create({
  container: {
    padding: 10,
    height: 60,
  },
  row: {
    flexDirection: 'row',
    justifyContent: 'space-between',
  },
  right: {
    flex: 1,
    alignSelf: 'flex-end',
    alignItems: 'flex-end',
  },
  text: {
    color: '#161616',
    fontSize: 16,
    fontWeight: '500',
  },
  name: {
    color: 'rgba(0,0,0,0.5)',
    fontSize: 12,
    fontWeight: '300',
  },
});

You may now uncomment the Profile component inside screens/ProfileScreen.js. If you see a username, email and number of votes, well done!

link AuthLoading

We are nearly done with the authentication flow. Now we would like to use the app's persisted auth token to re-direct users when they return to the app.

Create a new screens/AuthLoading.js and add the following:

import React, { useEffect } from 'react';
import {
  ActivityIndicator,
  AsyncStorage,
  StatusBar,
  StyleSheet,
  View,
} from 'react-native';

export default function AuthLoadingScreen(props) {
  useEffect(() => {
    _bootstrapAsync();
  });

  // Fetch the token from storage then navigate to our appropriate place
  _bootstrapAsync = async () => {
    const userToken = await AsyncStorage.getItem('token');

    // This will switch to the App screen or Auth screen and this loading
    // screen will be unmounted and thrown away.
    props.navigation.replace(userToken ? 'Profile' : 'Login');
  };

  // Render any loading content that you like here
  return (
    <View>
      <ActivityIndicator />
      <StatusBar barStyle="default" />
    </View>
  );
}

Now let's wire it up with the ProfileStack. Modify your navigation/MainTabNavigator with the following changes:

// navigation/MainTabNavigator.js

// navigation/BottomTabNavigator.js

import { AsyncStorage } from 'react-native';

import AuthLoading from '../screens/AuthLoading';
import ProfileScreen from '../screens/ProfileScreen';
import MovieDetail from '../screens/MovieDetail';

/* ... */

function ProfileStack() {
  return (
    <Stack.Navigator>
      <Stack.Screen
        name="AuthLoading" 
        component={AuthLoading} 
      />
      <Stack.Screen
        name="Login" 
        component={LoginScreen} 
      />
      <Stack.Screen
        name="Profile" 
        component={ProfileScreen} 
        options={({ navigation, route }) => ({
          headerRight: props => (
          <Ionicons 
            onPress={async () => {
                await AsyncStorage.removeItem('token');
                navigation.replace('Login')
              } 
            }
            name="md-exit" 
            size={25} 
            color={"#161616"} 
            style={{
              position: 'relative',
              left: 20,
              zIndex: 8
            }} />
          ),
        })} />
    </Stack.Navigator>
  )
};

Perfect, the auth flow is now wired up for returning users to see their profile. Each user "session" is made by the current persisted auth token. This way, we can persist sessions, even when the user closes the app entirely.

link Voting

With the auth flow done we can focus on voting functionality. Let's revisit screens/MovieDetail.js for one final edit:

In the screens/MovieDetail.js top level imports, add the following queries and mutations:

import { useQuery, useMutation } from '@apollo/react-hooks';
import gql from 'graphql-tag';

// Queries
const VOTES_QUERY = gql`
  query {
    currentUser {
      id
      username
      email
      votes {
        movie {
          id
          title
          description
          imageUrl
          category {
            title
          }
        }
      }
    }
  }
`;

// Mutations
const ADD_VOTE = gql`
  mutation AddVote($movieId:ID!) {
    addVote(movieId: $movieId)
  }
`;

const REMOVE_VOTE = gql`
  mutation RemoveVote($movieId:ID!) {
    removeVote(movieId: $movieId)
  }
`;

Let's wire up the queries and mutations with the current view:

function MovieDetail({ navigation }) {
  const { data, refetch } = useQuery(VOTES_QUERY);
  const [addVote] = useMutation(ADD_VOTE);
  const [removeVote] = useMutation(REMOVE_VOTE);

  /* ... */

  const isFavorite = data && 
    data.currentUser.votes && 
      data.currentUser.votes && 
        data.currentUser.votes.find(vote => vote.movie.id == id);

  /* ... */

  return (
    /* ... */
        <RoundedButton
          text={saveString}
          textColor={primaryColor}
          backgroundColor={secondaryColor}
          onPress={() => {
            if (isFavorite) {
              removeVote({ variables: { movieId: parseFloat(id) } })
              .then(() => refetch())
              .catch(err => console.log(err)) 
            } else {
              addVote({ variables: { movieId: parseFloat(id) } })
              .then(() => refetch())
              .catch(err => console.log(err))
            }
          }}
          icon={<Ionicons name="md-checkmark-circle" size={20} color={primaryColor} style={styles.saveIcon} />}
        />
    /* ... */
  )

The MovieDetail screen now allows logged-in users to vote for their favorite films.

WELL DONE!

link Bonus Challenge

Number of Votes

Inside screens/MovieDetail.js display the number of votes for the movie.

Restricted Access

If an unauthenticated user tries to vote, alert them to log in or sign up.

Wrapping Up

Today we made a React Native app with authentication flow and associative logic. Up next we will combine the Movies app with our previous Posts app.

format_list_bulleted
help_outline