Chapter 7: Client Authentication

Full Stack TypeScript

link Client Authentication

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

  • Establish an authentication flow on their mobile apps
  • Persist and include tokens and keep users logged in
  • Fetch the current user's profile information

Introduction

In order to allow users to create Places, we require them to sign up for our service. This prevents some bad actors from posting anonymous NSFW content.

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 Client Credentials

The setup for this project deviates from previous React Native clients. We will be using a authentication-ready client, thanks to the following new dependencies:

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

yarn add apollo-link apollo-utilities

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

Let's add the following HttpLink, request and requestLink variables inside graphql/index.ts, to initialize a new ApolloClient.

import ApolloClient from 'apollo-client';
import { InMemoryCache } from 'apollo-cache-inmemory';
import { HttpLink } from 'apollo-link-http';
import { AsyncStorage } from 'react-native';
import { ApolloLink, Observable } from 'apollo-link';

const link = new HttpLink({
  uri: 'http://localhost:4000/graphql',
  credentials: 'include'
});

const request = async operation => {
  const token = await AsyncStorage.getItem('token');
  operation.setContext({
    headers: {
      authorization: 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();
      };
    })
);

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

export * from './graphql-hooks';

We now have a GraphQL client that will include an auth token from AsyncStorage. Let's utilize this functionality with our new authentication flow.

Click here to learn more about AsyncStorage.

Click here to learn more about SecureStore.

link Generate Queries

One thing we seem to be missing are each place's user model. Let's update the app to include users with places!

Generating the new GraphQL requests is easier than ever. Let's return to graphql/places.graphql, and add the following:

query Places {
  places {
    id
    title
    description
    imageUrl
    creationDate
+    user {
+      id
+      username
+    }
  }
}

Let's add another new file called graphql/currentUser.graphql:

query CurrentUser {
  currentUser {
    id
    username
    email
    places {
      id
      title
      description
      imageUrl
    }
  }
}

To see the changes in action, reload the application. You may see each place's user's information.

link Generate Mutations

Create a new file called graphql/login.graphql:

mutation SignIn($username: String!, $password: String!) {
  login(input: { username: $username, password: $password }) {
    token
    user {
      id
      username
      email
    }
  }
}

Create a new file called graphql/register.graphql:

mutation SignUp($username: String!, $email: String!, $password: String!) {
  register(input:{ username: $username, email: $email, password: $password}) {
    token
    user {
      id
      username
      email
    }
  }
}

Now let's try to generate the mutation hooks! With the server running, go ahead and re-generate the GraphQL queries:

yarn generate

link AuthLoading

For our new "Auth" tab, we create a loading screen to indicate the loading state.

Create a new file called src/screens/AuthLoading.tsx:

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

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

  // Fetch the token from storage then navigate to our appropriate place
  const 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 style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}>
      <ActivityIndicator />
      <StatusBar barStyle="default" />
    </View>
  );
}

link LoginScreen

The LoginScreen will be the first screen during user auth flow. With the new queries generated, we can reference them in a new file called src/screens/LoginScreen:

import React, { useState } from 'react';
import {
  View,
  StyleSheet,
  Dimensions,
  AsyncStorage,
  ScrollView
} from 'react-native';
import { useSignUpMutation, useSignInMutation } from '../../graphql';
import { Button, TextInput, useTheme } from 'react-native-paper';

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

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

  // Signing Up
  const [signUpMutation] = useSignUpMutation({
    async onCompleted({ register }) {
      const { token } = register;
      if (token) {
        try {
          await AsyncStorage.setItem('token', token);
          navigation.replace('Profile');
        } catch (err) {
          console.log(err.message);
        }
      }
    }
  });

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

  return (
    <ScrollView
      contentContainerStyle={[
        styles.container,
        { backgroundColor: theme.colors.background }
      ]}
    >
      {login ? null : (
        <TextInput
          onChangeText={text => setUsername(text)}
          value={username}
          placeholder="Username"
          label="Username"
          mode="outlined"
          autoCorrect={false}
          autoCapitalize="none"
          style={styles.input}
        />
      )}
      <TextInput
        onChangeText={text => setEmail(text)}
        value={email}
        placeholder={login ? 'Email or Username' : 'Email'}
        label={login ? 'Email or Username' : 'Email'}
        mode="outlined"
        autoCorrect={false}
        autoCapitalize="none"
        style={styles.input}
      />
      <TextInput
        onChangeText={text => setPassword(text)}
        value={password}
        placeholder="Password"
        label="Password"
        mode="outlined"
        autoCorrect={false}
        autoCapitalize="none"
        style={styles.input}
        secureTextEntry
      />
      <View style={styles.buttonContainer}>
        <Button
          labelStyle={{ color: theme.colors.text }}
          style={{
            backgroundColor: theme.colors.accent,
            marginTop: 20
          }}
          onPress={() => {
            if (login) {
              // email validation
              const isEmail = email.includes('@');
              isEmail
                ? signInMutation({
                    variables: { email, password }
                  })
                : signInMutation({
                    variables: { username: email, password }
                  });
            } else {
              signUpMutation({ variables: { email, username, password } });
            }
          }}
        >
          {login ? 'Login' : 'Sign Up'}
        </Button>
        <Button
          style={{ marginTop: 20 }}
          onPress={() => {
            setLogin(!login);
          }}
          icon="information"
        >
          {login ? 'Need an account? Sign Up' : 'Have an account? Login'}
        </Button>
      </View>
    </ScrollView>
  );
}

LoginScreen.navigationOptions = {
  title: 'Welcome'
};

const styles = StyleSheet.create({
  container: {
    flex: 1,
    justifyContent: 'flex-start',
    alignItems: 'center',
    paddingHorizontal: 15,
    paddingVertical: 20
  },
  input: {
    width: width - 40,
    height: 60,
    marginTop: 5
  },
  buttonContainer: {
    width: '100%'
  }
});

Export the new LoginScreen and import it to src/navigation/MainTabNavigator:

import { Places, Login } from '../screens';

export const ProfileStack = () => {
  return (
    <Stack.Navigator
      initialRouteName="Auth"
      headerMode="screen"
      screenOptions={{
        header: ({ scene, previous, navigation }) => (
          <Header scene={scene} previous={previous} navigation={navigation} />
        )
      }}
    >
      <Stack.Screen name="Auth" component={AuthLoading} />
      <Stack.Screen name="Login" component={Login} />
    </Stack.Navigator>
  );
};

link ProfileScreen

Each time a user signs in or registers, we will navigate to their profile. Let's create a new file called src/screens/ProfileScreen.tsx:

import React from 'react';
import { View, SafeAreaView, FlatList } from 'react-native';
import { Card, Avatar, ActivityIndicator } from 'react-native-paper';
import { useCurrentUserQuery } from '../../graphql';
import { CardView } from '../components';

interface Props {
  navigation;
}

const Profile: React.FC<Props> = props => {
  const { navigation } = props;
  const { data, loading } = useCurrentUserQuery({
    fetchPolicy: 'network-only'
  });
  if (loading) {
    return (
      <View style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}>
        <ActivityIndicator />
      </View>
    );
  }
  return (
    <View>
      <Card>
        <Card.Title
          title={(data.currentUser && data.currentUser.username) || ''}
          subtitle={(data.currentUser && data.currentUser.email) || ''}
          left={props => <Avatar.Icon {...props} icon="account" />}
        />
      </Card>
      <FlatList
        data={
          data && data.currentUser && data.currentUser.places
            ? data.currentUser.places
            : []
        }
        keyExtractor={item => `${item.id}`}
        renderItem={({ item }) => {
          return (
            <CardView
              {...(item as any)}
              onPress={() =>
                navigation.navigate('Detail', {
                  item: { ...item, user: data.currentUser }
                })
              }
            />
          );
        }}
      />
    </View>
  );
};

export default Profile;

link Profile Route

Wire up our new ProfileScreen to the existing routes inside src/screens/index.ts:

import Places from './Places';
import PlaceDetail from './PlaceDetail';
import AuthLoading from './AuthLoading';
import Login from './LoginScreen';
import Profile from './ProfileScreen';

export { Places, PlaceDetail, AuthLoading, Login, Profile };

Finally, import and add the new Profile to our ProfileStack inside src/navigation/MainTabNavigator:

-import { PlaceDetail, AuthLoading, Login } from '../screens';
+import { PlaceDetail, AuthLoading, Login, Profile } from '../screens';

export const ProfileStack = () => {
  return (
    <Stack.Navigator
      initialRouteName="Auth"
      headerMode="screen"
      screenOptions={{
        header: ({ scene, previous, navigation }) => (
          <Header scene={scene} previous={previous} navigation={navigation} />
        )
      }}
    >
      <Stack.Screen name="Auth" component={AuthLoading} />
      <Stack.Screen name="Login" component={Login} />
+      <Stack.Screen name="Profile" component={Profile} />
    </Stack.Navigator>
  );
};

Profile Stack

Be sure to add the new ProfileStack to src/navigation/MainTabNavigator.ts:

<Tab.Navigator
  initialRouteName="Places"
  // shifting={true}
  sceneAnimationEnabled={false}
>
  <Tab.Screen
    name="Places"
    component={PlaceStack}
    options={{
      tabBarIcon: 'home-account'
    }}
  />
+  <Tab.Screen
+    name="Profile"
+    component={ProfileStack}
+    options={{
+      tabBarIcon: 'bell-outline'
+    }}
+  />
</Tab.Navigator>
  • Try login or sign up, you should be able to see the new profile screen.
  • Try logging out, you should see the log in screen.

WELL DONE!

Wrapping Up

Today we added Tab and Drawer Navigation to our app. Up next, we integrate Full CRUD (Create, Read, Update, Destroy) for our Places.

format_list_bulleted
help_outline