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.