Bootcamp Notes – Day 9 (Mon) – React Native: Week 4: Accessing Native Capabilities of Devices: The Expo SDK

Accessing Native Capabilities of Devices: The Expo SDK

Overview of Week 4

We will begin with an introduction to the expo SDK. Of course you have been using expo all along EXPO CLi and the snack code playground. This week we will dig into it SDK or software development kit, which provides us with a collection of APi’s for accessing native capabilities of mobile devices. We will start by taking a look at how to use the Expo secure store APi to save small amounts of encrypted data, such as username and password and will develop a new login screen for doing so using the reservation form you learn about using the Expo notifications APi for generating local notifications that you can see in the bar at the top of the device screen as well as in the notifications drawer, that can be dragged down from the top of the screen. Will also learn to use the Expo mail composer API to access the device’s email client from the contact screen. We will also learn to use React Native’s own built in share APi to easily share information about a campsite to other applications via the campsite info screen. You will see an example of building tab navigation with react navigations create bottom tab navigator and with that you will add a register tab to your login screen and use Expo’s image picker APi to access the devices camera and take a photo. Finally, you will learn to use a third-party library called netinfo to obtain network information, such as the network type from the device and respond to it. Along the way, you will be exposed to a newer way of working with promises called async await, which can be used instead of the then method syntax, you have learned before.


 Overview: The Expo SDK

Expo is a framework and a platform for universal React applications. It is a set of tools and services built around React Native and native platforms that help you develop, build, deploy, and quickly iterate on iOS, Android, and web apps from the same JavaScript/TypeScript codebase.

The tools provided by EXPO are:

  1. EXPO CLIENT
  2. EXPO CLi
  3. EXPO SNACK
  4. EXPO SDK

EXPO CLi starts Metro Bundler, which is an HTTP server that compiles the JavaScript code of our app using Babel and serves it to the Expo app. It also pops up Expo Dev Tools, a graphical interface for EXPO Cli.

If you have been testing your app using a real mobile device, then you have been using the Expo Client, which lets you test your app on your mobile device, such as the phone or tablet by connecting to the metrobundler htttp.

Snack is the codeplayground for react native applications

SDK (Software Development Kit)

React Native itself is concerned with giving you the tools to build the front end of your application, creating different Ui components such as views, buttons, text, controlling the styles and so on! It does not concern itself too much with reaching into the native device capabilities and that’s where the Expo SDK comes in.

The EXPO SDK provides access to device and system functionality such as: contacts, camera, GPS location, gyroscope, pedometer and much more! These APi are developed by the Expo team using the native code for each platform, then packaged for use with JavaScript. There are literally dozens of APi’s from Expo. This week, we will be working with accessing the camera and image gallery, along with device storage using the EXPO secure store, sending local notifications and sending email.

Why Not Expo? Expo Limitations

Expo does have its limitations for many applications that you might develop. These limitations may not be much of concern, but if you continue to advance in mobile development, you will probably start running into some special cases, where you need to go beyond what EXPO can offer. Expo is upfront about its own limitations and in their docs they have a section that lists them. One of it’s limitations is that it does not support all Android and iOS native APi’s. Expo has converted many of them for use of React Native, but not all. Also, the Expo CLi provides eject command that will expose the underlying native code and release your project from the managed workflow. Then you can continue developing the project using native code such as Java for Android or swift for iOS to add the functionality that you need. So there is really not a downside to starting out your project with the Expo then if you later run into a limitation you can eject. Expo is being updated all the time. Also the SDK does not support all the types of background execution.


 Overview: Secure Store

In week three when we implemented Redux Persist, we mentioned that there are multiple types of storage available for the mobile device such as: localStorage, AsyncStorage, Expo Secure Store, and more!

If you are storing data in a key value format and you don’t need it to be encrypted for application settings for example. The React Native AsyncStorage storage APi would be a good option. If you need to use a more secure form of storage on your device. Then the EXPO SDK provides an APi called Secure Store. Secure Store allows you to store data as encrypted, key value pairs on your device. It is meant to be used for small amounts of data. In iOS the value is stored using what’s called keychain services in iOS development. On Android the values are stored in shared preferences and encrypted in what’s called androids key store system. You do not have to know about these to use the secure store. It is just an example of how Expo takes native code and leverages it to build an APi that can be used for both operating systems.

  • Unencrypted key-value storage: React Native Asyn Storage APi
  • Expo Secure Store APi: encrypted key-value storage
    • meant to be used for small amounts of sensitive date
    • uses iOS keychain services and Android SharedPreferences/Keystore system

The Expo Secure Store APi exposes three methods:

  • SecureStore.setItemAsync(key, value, options)
  • SecureStore.getItemAsync(key, options)
  • SecureStore.deleteItemAsync(key, options)

These methods all take a key as the required first argument and setItemAsync has a required second argument of value and they all have an additional optional options argument. These methods do just what you expect them to do from the names. What is important to know about them is that they will each return a promise that will reject if there was an error with the operation.

  • SecureStore.setItemAsync(key, value, options)
    • returns a promise that will reject if value cannot be stored on the device
  • SecureStore.getItemAsync(key, options)
    • returns a promise that will resolve to stored value, or resolve to null if no entry for given key, or reject if error occurred
  • SecureStore.deleteItemAsync(key, options)
    • returns a promise that will reject if the value could not be deleted

We will use these in the following exercise, creating a component called login.


Secure Store

  • Learn to use the Expo SecureStore API with an example of how to use each of its three methods: getItemAsync, setItemAsync, deleteItemAsync.
  • Create a new Login component that contains a form, and uses the SecureStore API to store the login credentials.
  • Install Secure Store:  expo install expo-secure-store

LoginComponent.js

import React, { Component } from 'react';
import { View, Button, StyleSheet } from 'react-native';
import { Input, CheckBox } from 'react-native-elements';
import * as SecureStore from 'expo-secure-store';

class Login extends Component {

    constructor(props) {
        super(props);

        this.state = {
            username: '',
            password: '',
            remember: false
        };
    }

    static navigationOptions = {
        title: 'Login'
    }

    handleLogin() {
        console.log(JSON.stringify(this.state));
        if (this.state.remember) {
            SecureStore.setItemAsync(
                'userinfo',
                JSON.stringify({
                    username: this.state.username,
                    password: this.state.password
                })
            ).catch(error => console.log('Could not save user info', error));
        } else {
            SecureStore.deleteItemAsync('userinfo').catch(
                error => console.log('Could not delete user info', error)
            );
        }
    }

    componentDidMount() {
        SecureStore.getItemAsync('userinfo')
            .then(userdata => {
                const userinfo = JSON.parse(userdata);
                if (userinfo) {
                    this.setState({username: userinfo.username});
                    this.setState({password: userinfo.password});
                    this.setState({remember: true})
                }
            });
    }

    render() {
        return (
            <View style={styles.container}>
                <Input
                    placeholder='Username'
                    leftIcon={{type: 'font-awesome', name: 'user-o'}}
                    onChangeText={username => this.setState({username})}
                    value={this.state.username}
                    containerStyle={styles.formInput}
                    leftIconContainerStyle={styles.formIcon}
                />
                <Input
                    placeholder='Password'
                    leftIcon={{type: 'font-awesome', name: 'key'}}
                    onChangeText={password => this.setState({password})}
                    value={this.state.password}
                    containerStyle={styles.formInput}
                    leftIconContainerStyle={styles.formIcon}
                />
                <CheckBox
                    title='Remember Me'
                    center
                    checked={this.state.remember}
                    onPress={() => this.setState({remember: !this.state.remember})}
                    containerStyle={styles.formCheckbox}
                />
                <View style={styles.formButton}>
                    <Button
                        onPress={() => this.handleLogin()}
                        title='Login'
                        color='#5637DD'
                    />
                </View>
            </View>
        );
    }
}

const styles = StyleSheet.create({
    container: {
        justifyContent: 'center',
        margin: 20
    },
    formIcon: {
        marginRight: 10
    },
    formInput: {
        padding: 10
    },
    formCheckbox: {
        margin: 10,
        backgroundColor: null
    },
    formButton: {
        margin: 40
    }
});

export default Login;

 

MainComponent.js

import React, { Component } from ‘react’;
import Home from ‘./HomeComponent’;
import Directory from ‘./DirectoryComponent’;
import CampsiteInfo from ‘./CampsiteInfoComponent’;
import About from ‘./AboutComponent’;
import Contact from ‘./ContactComponent’;
import Reservation from ‘./ReservationComponent’;
import Favorites from ‘./FavoritesComponent’;
import { View, Platform, StyleSheet, Text, ScrollView, Image } from ‘react-native’;
import { createStackNavigator } from ‘react-navigation-stack’;
import { createDrawerNavigator, DrawerItems } from ‘react-navigation-drawer’;
import { createAppContainer } from ‘react-navigation’;
import { Icon } from ‘react-native-elements’;
import SafeAreaView from ‘react-native-safe-area-view’;
import { connect } from ‘react-redux’;
import { fetchCampsites, fetchComments, fetchPromotions, fetchPartners } from ‘../redux/ActionCreators’;
import Login from ‘./LoginComponent’;
const mapDispatchToProps = {
    fetchCampsites,
    fetchComments,
    fetchPromotions,
    fetchPartners
};
const DirectoryNavigator = createStackNavigator(
    {
        Directory: { 
            screen: Directory,
            navigationOptions: ({navigation}) => ({
                headerLeft: <Icon
                    name=’list’
                    type=’font-awesome’
                    iconStyle={styles.stackIcon}
                    onPress={() => navigation.toggleDrawer()}
                />
            })
        },
        CampsiteInfo: { screen: CampsiteInfo }
    },
    {
        initialRouteName: ‘Directory’,
        defaultNavigationOptions: {
            headerStyle: {
                backgroundColor: ‘#5637DD’
            },
            headerTintColor: ‘#fff’,
            headerTitleStyle: {
                color: ‘#fff’
            }
        }
    }
);
const HomeNavigator = createStackNavigator(
    {
        Home: { screen: Home }
    },
    {
        defaultNavigationOptions: ({navigation}) => ({
            headerStyle: {
                backgroundColor: ‘#5637DD’
            },
            headerTintColor: ‘#fff’,
            headerTitleStyle: {
                color: ‘#fff’
            },
            headerLeft: <Icon
                name=’home’
                type=’font-awesome’
                iconStyle={styles.stackIcon}
                onPress={() => navigation.toggleDrawer()}
            />
        })
    }
);
const AboutNavigator = createStackNavigator(
    {
        About: { screen: About }
    },
    {
        defaultNavigationOptions: ({navigation}) => ({
            headerStyle: {
                backgroundColor: ‘#5637DD’
            },
            headerTintColor: ‘#fff’,
            headerTitleStyle: {
                color: ‘#fff’
            },
            headerLeft: <Icon
                name=’info-circle’
                type=’font-awesome’
                iconStyle={styles.stackIcon}
                onPress={() => navigation.toggleDrawer()}
            />
        })
    }
);
const ContactNavigator = createStackNavigator(
    {
        Contact: { screen: Contact }
    },
    {
        defaultNavigationOptions: ({navigation}) => ({
            headerStyle: {
                backgroundColor: ‘#5637DD’
            },
            headerTintColor: ‘#fff’,
            headerTitleStyle: {
                color: ‘#fff’
            },
            headerLeft: <Icon
                name=’address-card’
                type=’font-awesome’
                iconStyle={styles.stackIcon}
                onPress={() => navigation.toggleDrawer()}
            />
        })
    }
);
const ReservationNavigator = createStackNavigator(
    {
        Reservation: { screen: Reservation }
    },
    {
        defaultNavigationOptions: ({navigation}) => ({
            headerStyle: {
                backgroundColor: ‘#5637DD’
            },
            headerTintColor: ‘#fff’,
            headerTitleStyle: {
                color: ‘#fff’
            },
            headerLeft: <Icon
                name=’tree’
                type=’font-awesome’
                iconStyle={styles.stackIcon}
                onPress={() => navigation.toggleDrawer()}
            />
        })
    }
);
const FavoritesNavigator = createStackNavigator(
    {
        Favorites: { screen: Favorites }
    },
    {
        defaultNavigationOptions: ({navigation}) => ({
            headerStyle: {
                backgroundColor: ‘#5637DD’
            },
            headerTintColor: ‘#fff’,
            headerTitleStyle: {
                color: ‘#fff’
            },
            headerLeft: <Icon
                name=’heart’
                type=’font-awesome’
                iconStyle={styles.stackIcon}
                onPress={() => navigation.toggleDrawer()}
            />
        })
    }
);
const LoginNavigator = createStackNavigator(
    {
        Login: { screen: Login }
    },
    {
        defaultNavigationOptions: ({navigation}) => ({
            headerStyle: {
                backgroundColor: ‘#5637DD’
            },
            headerTintColor: ‘#fff’,
            headerTitleStyle: {
                color: ‘#fff’
            },
            headerLeft: <Icon
                name=’sign-in’
                type=’font-awesome’
                iconStyle={styles.stackIcon}
                onPress={() => navigation.toggleDrawer()}
            />
        })
    }
);
const CustomDrawerContentComponent = props => (
    <ScrollView>
        <SafeAreaView 
            style={styles.container}
            forceInset={{top: ‘always’, horizontal: ‘never’}}>
            <View style={styles.drawerHeader}>
                <View style={{flex: 1}}>
                    <Image source={require(‘./images/logo.png’)} style={styles.drawerImage} />
                </View>
                <View style={{flex: 2}}>
                    <Text style={styles.drawerHeaderText}>NuCamp</Text>
                </View>
            </View>
            <DrawerItems {…props} />
        </SafeAreaView>
    </ScrollView>
);
const MainNavigator = createDrawerNavigator(
    {
        Login: {
            screen: LoginNavigator,
            navigationOptions: {
                drawerIcon: ({tintColor}) => (
                    <Icon
                        name=’sign-in’
                        type=’font-awesome’
                        size={24}
                        color={tintColor}
                    />
                )
            }
        },
        Home: {
            screen: HomeNavigator,
            navigationOptions: {
                drawerIcon: ({tintColor}) => (
                    <Icon
                        name=’home’
                        type=’font-awesome’
                        size={24}
                        color={tintColor}
                    />
                )
            }
        },
        Directory: {
            screen: DirectoryNavigator,
            navigationOptions: {
                drawerIcon: ({tintColor}) => (
                    <Icon
                        name=’list’
                        type=’font-awesome’
                        size={24}
                        color={tintColor}
                    />
                )
            }
        },
        Reservation: {
            screen: ReservationNavigator,
            navigationOptions: {
                drawerLabel: ‘Reserve Campsite’,
                drawerIcon: ({tintColor}) => (
                    <Icon
                        name=’tree’
                        type=’font-awesome’
                        size={24}
                        color={tintColor}
                    />
                )
            }
        },
        Favorites: {
            screen: FavoritesNavigator,
            navigationOptions: {
                drawerLabel: ‘My Favorites’,
                drawerIcon: ({tintColor}) => (
                    <Icon
                        name=’heart’
                        type=’font-awesome’
                        size={24}
                        color={tintColor}
                    />
                )
            }
        },
        About: {
            screen: AboutNavigator,
            navigationOptions: {
                drawerLabel: ‘About Us’,
                drawerIcon: ({tintColor}) => (
                    <Icon
                        name=’info-circle’
                        type=’font-awesome’
                        size={24}
                        color={tintColor}
                    />
                )
            }
        },
        Contact: {
            screen: ContactNavigator,
            navigationOptions: {
                drawerLabel: ‘Contact Us’,
                drawerIcon: ({tintColor}) => (
                    <Icon
                        name=’address-card’
                        type=’font-awesome’
                        size={24}
                        color={tintColor}
                    />
                )
            }
        }
    },
    {
        initialRouteName: ‘Home’,
        drawerBackgroundColor: ‘#CEC8FF’,
        contentComponent: CustomDrawerContentComponent
    }
);
const AppNavigator = createAppContainer(MainNavigator)
class Main extends Component {
    componentDidMount() {
        this.props.fetchCampsites();
        this.props.fetchComments();
        this.props.fetchPromotions();
        this.props.fetchPartners();
    }
    render() {
        return (
            <View style={{
                flex: 1,
                paddingTop: Platform.OS === ‘ios’ ? 0 : Expo.Constants.statusBarHeight
            }}>
                <AppNavigator />
            </View>
        );
    }
}
const styles = StyleSheet.create({
    container: {
        flex: 1,
    },
    drawerHeader: {
        backgroundColor: ‘#5637DD’,
        height: 140,
        alignItems: ‘center’,
        justifyContent: ‘center’,
        flex: 1,
        flexDirection: ‘row’
    },
    drawerHeaderText: {
        color: ‘#fff’,
        fontSize: 24,
        fontWeight: ‘bold’
    },
    drawerImage: {
        margin: 10,
        height: 60,
        width: 60
    },
    stackIcon: {
        marginLeft: 10,
        color: ‘#fff’,
        fontSize: 24
    }
});
export default connect(null, mapDispatchToProps)(Main);

Additional Resources: