[codorial] WIP authentication tutorial
This commit is contained in:
parent
e73812a1e0
commit
9adde8be34
|
@ -0,0 +1,21 @@
|
||||||
|
{
|
||||||
|
"title": "Authentication with Firebase",
|
||||||
|
"steps": [
|
||||||
|
{
|
||||||
|
"title": "Getting Started",
|
||||||
|
"file": "getting-started"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "Project Structure",
|
||||||
|
"file": "project-structure"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "Understanding Firebase Auth",
|
||||||
|
"file": "understanding-firebase-auth"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "Integrating Redux",
|
||||||
|
"file": "integrating-redux"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
|
@ -0,0 +1,43 @@
|
||||||
|
# Getting Started
|
||||||
|
|
||||||
|
Welcome to the 'Authentication with Firebase' Codorial using the [react-native-firebase](https://rnfirebase.io) library.
|
||||||
|
The various steps of the Codorial will cover how to setup your application to require both email/password login and social login using Facebook,
|
||||||
|
to handling the users authenticated state using the popular [redux](https://redux.js.org/introduction) library whilst also considering integrate routing
|
||||||
|
using [react-navigation](https://reactnavigation.org/).
|
||||||
|
|
||||||
|
## Prerequisites
|
||||||
|
|
||||||
|
This Codorial assumes you know the basics of the following topics:
|
||||||
|
|
||||||
|
- ES6 JavaScript.
|
||||||
|
- Starting your app using an emulator on Android/iOS.
|
||||||
|
- Understand how to setup a new Firebase project.
|
||||||
|
- Debugging with React Native.
|
||||||
|
- Managing your project using Android Stuido and/or XCode.
|
||||||
|
- Installation of the [react-native-firebase](https://rnfirebase.io) library (see "Creating a base project" below).
|
||||||
|
|
||||||
|
This Codorial was created with React Native version `0.53.0`.
|
||||||
|
|
||||||
|
## Creating a base project
|
||||||
|
|
||||||
|
This project will take a bare bones React Native setup and explain every step required to implement a solid authentication flow in your application.
|
||||||
|
To start we need a base project to work from.
|
||||||
|
|
||||||
|
Both options below require you to setup a new Firebase project and add the configuration file to your project - check out the [documentation](https://rnfirebase.io/docs/v3.2.x/installation/initial-setup) on how to do that if needed.
|
||||||
|
|
||||||
|
### Option 1: Using `react-native init`
|
||||||
|
|
||||||
|
You can quickly create a base React Native project using `react-native init` by following the React Native [documentation](http://facebook.github.io/react-native/docs/getting-started.html).
|
||||||
|
|
||||||
|
> Ensure you follow the "Building Projects with Native Code" tab, as the project won't work using Expo due to requiring native modules.
|
||||||
|
|
||||||
|
Once installed, you need to install the [react-native-firebase](https://rnfirebase.io/docs/v3.2.x/installation/initial-setup) library. Ensure you've
|
||||||
|
also installed the Authentication module on your platform ([Android](https://rnfirebase.io/docs/v3.2.x/auth/android) or [iOS](https://rnfirebase.io/docs/v3.2.x/auth/ios))!
|
||||||
|
|
||||||
|
### Option 2: Using [react-native-firebase-starter](https://github.com/invertase/react-native-firebase-starter)
|
||||||
|
|
||||||
|
A starter kit has been created to help you get up and running with minimal setup needed. If you're new to React Native this will be perfect starting point.
|
||||||
|
|
||||||
|
> Keep in mind every Firebase module is installed in this starter kit. You can refer to the react-native-firebase [documentation](https://rnfirebase.io/docs) if you want to remove
|
||||||
|
any unwanted modules.
|
||||||
|
|
|
@ -0,0 +1,82 @@
|
||||||
|
# Integrating Redux
|
||||||
|
|
||||||
|
Redux has become somewhat of a buzz word in the React community, and is generally used in most projects without thought. This Codorial
|
||||||
|
won't go into details on what it is as their own [documentation](https://redux.js.org/introduction/motivation) does a wonderful job at explaining
|
||||||
|
what it's for and why to use it.
|
||||||
|
|
||||||
|
*TLDR* Redux provides your app with a single "state" (data), which can be accessed by any component. You can subscribe to this data to cause
|
||||||
|
a component update whenever something changes, even if it's deeply nested.
|
||||||
|
|
||||||
|
Although the end product of this Codorial certain doesn't require Redux to function, as your app grows in complexity Redux becomes more and
|
||||||
|
more important to manage your data.
|
||||||
|
|
||||||
|
## Installing Redux
|
||||||
|
|
||||||
|
Lets go ahead by installing the core Redux library and the React bindings:
|
||||||
|
|
||||||
|
```
|
||||||
|
npm install --save redux react-redux
|
||||||
|
```
|
||||||
|
|
||||||
|
Now within our projects `src` directory, create a `store.js` file. This file will contain all of our Redux logic, however you may want to break
|
||||||
|
this out into multiple directories as your projects grows in complexity.
|
||||||
|
|
||||||
|
```
|
||||||
|
// src/store.js
|
||||||
|
import { createStore } from 'redux';
|
||||||
|
|
||||||
|
// Create a reducer (see below for explanation)
|
||||||
|
function reducer(state, action) {
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default createStore(reducer);
|
||||||
|
```
|
||||||
|
|
||||||
|
> You may want to consider installing the [redux-logger](https://github.com/evgenyrodionov/redux-logger) library.
|
||||||
|
|
||||||
|
### Reducer
|
||||||
|
|
||||||
|
A reducer is a simple JavaScript function which takes two arguments: `state` & `action`. The idea of a reducer is to take "some data" from an `action`
|
||||||
|
and return new state.
|
||||||
|
|
||||||
|
- `state` is any sort of data which cannot be altered (immutable). A reducer must return a new value each time.
|
||||||
|
- `action` is an object containing a `type`, and any unreduced data. More on this later.
|
||||||
|
|
||||||
|
## Integrating Redux into the app
|
||||||
|
|
||||||
|
Our Redux store is now ready to be used. `react-redux` provides us with a `Provider` component which "provides" any children
|
||||||
|
with access to the store via [context](https://reactjs.org/docs/context.html). Luckily we don't need to worry about this too much as the lirbary
|
||||||
|
takes care of the hard work!
|
||||||
|
|
||||||
|
Back within our original bootstrap file, we'll wrap the `App` component in the `Provider` component, so our business logic has access to Redux.
|
||||||
|
|
||||||
|
```js
|
||||||
|
// src/index.js
|
||||||
|
|
||||||
|
import React, { Component } from 'react';
|
||||||
|
import { Provider } from 'react-redux'; // Import the Provider component
|
||||||
|
|
||||||
|
import App from './App';
|
||||||
|
import store from './store';
|
||||||
|
|
||||||
|
function bootstrap() {
|
||||||
|
|
||||||
|
// Init any external libraries here!
|
||||||
|
|
||||||
|
return class extends Component {
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<Provider store={store}>
|
||||||
|
<App />
|
||||||
|
</Provider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default bootstrap;
|
||||||
|
```
|
||||||
|
|
||||||
|
Our app now has access to the power of Redux!
|
|
@ -0,0 +1,124 @@
|
||||||
|
# Project Structure
|
||||||
|
|
||||||
|
Although it may seem trivial, having a good initial project structure ensures you code will be clean and reusable. The following step gives
|
||||||
|
an opinionated guide to how this might look, which will work across both Android & iOS.
|
||||||
|
|
||||||
|
## Entry file
|
||||||
|
|
||||||
|
Every fresh React Native project contains to key files, an `index.android.js` & a `index.ios.js` files which currently individually render a simple React component
|
||||||
|
with basic styling. Rather than having two separate files, we're going to create a single file so both Android & iOS use it.
|
||||||
|
|
||||||
|
We'll achieve this by creating a `src` directory where our own code for the app will live. Create the directory with an `index.js` file, so your
|
||||||
|
project structure resembles the following:
|
||||||
|
|
||||||
|
```
|
||||||
|
- node_modules/
|
||||||
|
- android/
|
||||||
|
- ios/
|
||||||
|
- src/
|
||||||
|
-- index.js
|
||||||
|
- index.js
|
||||||
|
```
|
||||||
|
|
||||||
|
Now we can reference our bootstrap file in the `index.js` file, so both our platform share the same entry point:
|
||||||
|
|
||||||
|
```js
|
||||||
|
// index.js
|
||||||
|
|
||||||
|
import { AppRegistry } from 'react-native';
|
||||||
|
import bootstrap from './src';
|
||||||
|
|
||||||
|
AppRegistry.registerComponent('RNFirebaseStarter', () => bootstrap());
|
||||||
|
```
|
||||||
|
|
||||||
|
## Bootstrapping your project
|
||||||
|
|
||||||
|
You may have noticed before, but the `bootstrap` import is a function. This allows us to setup or initialize any external modules before our
|
||||||
|
React based application kick starts (such as [react-native-i18n](https://github.com/AlexanderZaytsev/react-native-i18n)).
|
||||||
|
|
||||||
|
Lets go ahead and setup our bootstrap file:
|
||||||
|
|
||||||
|
```js
|
||||||
|
// src/index.js
|
||||||
|
|
||||||
|
import React, { Component } from 'react';
|
||||||
|
import { View, Text } from 'react-native';
|
||||||
|
|
||||||
|
function bootstrap() {
|
||||||
|
|
||||||
|
// Init any external libraries here!
|
||||||
|
|
||||||
|
return class extends Component {
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<View>
|
||||||
|
<Text>
|
||||||
|
Bootstrapped!
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default bootstrap;
|
||||||
|
```
|
||||||
|
|
||||||
|
Although this function simply returns a basic React component, later we'll be able to see the power of having a bootstrap file which
|
||||||
|
consumes our entire application.
|
||||||
|
|
||||||
|
Go ahead and boot up your app onto your emulator. You should simply be presented with a plain screen with the words "Bootstrapped!".
|
||||||
|
|
||||||
|
TODO image
|
||||||
|
|
||||||
|
Although a good starting point, we want to separate we'll our business logic out of the bootstrap file, keeping it purely for app
|
||||||
|
initialization purposes. This can simply be done by creating a basic React component called `App.js`, which will also live in the `src` directory;
|
||||||
|
|
||||||
|
```js
|
||||||
|
// src/App.js
|
||||||
|
|
||||||
|
import React, { Component } from 'react';
|
||||||
|
import { View, Text } from 'react-native';
|
||||||
|
|
||||||
|
class App extends Component {
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<View>
|
||||||
|
<Text>
|
||||||
|
Bootstrapped!
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
export default App;
|
||||||
|
```
|
||||||
|
|
||||||
|
Now we can reference this component within our bootstrap setup and return it from the bootstrap component:
|
||||||
|
|
||||||
|
```js
|
||||||
|
// src/index.js
|
||||||
|
|
||||||
|
import React, { Component } from 'react';
|
||||||
|
import App from './App';
|
||||||
|
|
||||||
|
function bootstrap() {
|
||||||
|
|
||||||
|
// Init any external libraries here!
|
||||||
|
|
||||||
|
return class extends Component {
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<App />
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default bootstrap;
|
||||||
|
```
|
|
@ -0,0 +1,200 @@
|
||||||
|
# Understanding Firebase Authentication
|
||||||
|
|
||||||
|
Before we dive into the logic of implementing authentication, it's first important to understand how the Firebase API, and how it handles authentication
|
||||||
|
with the various options we have.
|
||||||
|
|
||||||
|
As we're also working in React, it's important to understand how Firebase's asynchronous API fits in with Reacts many lifecycle methods.
|
||||||
|
Luckily [react-native-firebase](https://rnfirebase.io) follows the Firebase web SDK API making this a breeze!
|
||||||
|
|
||||||
|
## Enabling authentication
|
||||||
|
|
||||||
|
Before we make a start, we need to tell Firebase that we plan on using authentication. We need to also enable a couple of the many login providers
|
||||||
|
which Firebase supports. Head over to the [Firebase console](https://console.firebase.google.com/u/0/) and select the project you're using.
|
||||||
|
|
||||||
|
Find the Authentication section and you'll be prompted with a number of options. To get started, we want to select the "SIGN-IN METHOD" tab:
|
||||||
|
|
||||||
|
TODO image
|
||||||
|
|
||||||
|
You'll see we have a number of options here, however for purposes of this Codorial we'll be using "Email/Password" and "Facebook" as our providers.
|
||||||
|
Go ahead and enable these:
|
||||||
|
|
||||||
|
TODO image
|
||||||
|
|
||||||
|
> If you don't have a Facebook app, simply enter dummy values. We'll cover this later on.
|
||||||
|
|
||||||
|
## Listening to the users authentication state
|
||||||
|
|
||||||
|
The Firebase API provides a simple yet powerful listener, which triggers when some event changes with the user.
|
||||||
|
This can be as obvious the user signing out or as subtle as the user validating their email address. Whatever the event, it triggers the same method: `onAuthStateChanged`.
|
||||||
|
|
||||||
|
```js
|
||||||
|
import firebase from 'react-native-firebase';
|
||||||
|
|
||||||
|
firebase.auth().onAuthStateChanged((user) => {
|
||||||
|
console.log(user);
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
The callback for the `onAuthStateChanged` method returns a single parameter, commonly referred to as `user`.
|
||||||
|
|
||||||
|
The concept here is simple;
|
||||||
|
|
||||||
|
- if a user is "signed in", our parameter will be a `class`, containing all sorts of information we know about the user,
|
||||||
|
from their e-mail address to any social provider IDs they may have signed in through.
|
||||||
|
- if the user signed out, the parameter will be `null` value.
|
||||||
|
- the method is immediately triggered when called.
|
||||||
|
|
||||||
|
> The `user` class provides a `.toJSON()` method to serialize the users details if required.
|
||||||
|
|
||||||
|
### Handling authentication state when the app closes
|
||||||
|
|
||||||
|
A common question we get is how to handle the users authenticated state when the app closes/restarts so they don't have to keep logging in each
|
||||||
|
time they open the app. Luckily this is all handled through Firebase so you don't have to worry about a thing - they'll only be signed out if they
|
||||||
|
choose to, or the app is uninstalled.
|
||||||
|
|
||||||
|
## Creating a new account
|
||||||
|
|
||||||
|
Creating a new account on Firebase is very easy. Another method called `createUserWithEmailAndPassword` is available which does exactly what it
|
||||||
|
says on the tin! This is an asynchronous promise which will throw an exception if something is wrong (such as email taken, or password too short).
|
||||||
|
Creating a user will also sign them in at the same time.
|
||||||
|
|
||||||
|
```js
|
||||||
|
import firebase from 'react-native-firebase';
|
||||||
|
|
||||||
|
firebase.auth().createUserWithEmailAndPassword('jim.bob@gmail.com', 'supersecret!')
|
||||||
|
.then((user) => {
|
||||||
|
console.log('New User', user);
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
console.error('Woops, something went wrong!, error);
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
What's great about this is we don't need to know about the user within the `then`, as any `onAuthStateChanged` listener would get triggered with our new
|
||||||
|
users details - how awesome is that.
|
||||||
|
|
||||||
|
## Signing into an existing account
|
||||||
|
|
||||||
|
Unsurprisingly, Firebase offers a method called `signInWithEmailAndPassword`, which follows the exact same flow as `createUserWithEmailAndPassword`:
|
||||||
|
|
||||||
|
```js
|
||||||
|
import firebase from 'react-native-firebase';
|
||||||
|
|
||||||
|
firebase.auth().signInWithEmailAndPassword('jim.bob@gmail.com', 'supersecret!')
|
||||||
|
.then((user) => {
|
||||||
|
console.log('Existing User', user);
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
console.error('Woops, something went wrong!, error);
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
## Using with React
|
||||||
|
|
||||||
|
Firebase on it's own is super simple, however when using in a React environment there's some gotchas you need to be mindful of.
|
||||||
|
|
||||||
|
### Handling state changes
|
||||||
|
|
||||||
|
For any React component to update, a state or prop change needs to occur. As our Firebase auth methods are asynchronous we cannot rely on
|
||||||
|
the data being available on component mount. To solve this issue, we can make use of state:
|
||||||
|
|
||||||
|
```js
|
||||||
|
import React, { Component } from 'react';
|
||||||
|
import { View, Text } from 'react-native';
|
||||||
|
import firebase from 'react-native-firebase';
|
||||||
|
|
||||||
|
class App extends React.Component {
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
this.state = {
|
||||||
|
loading: false,
|
||||||
|
user: null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
firebase.auth().onAuthStateChanged((user) => {
|
||||||
|
if (user) {
|
||||||
|
this.setState({
|
||||||
|
user: user.toJSON(), // serialize the user class
|
||||||
|
loading: false,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
this.setState({
|
||||||
|
loading: false,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { loading, user } = this.state;
|
||||||
|
|
||||||
|
// Firebase hasn't responded yet
|
||||||
|
if (loading) return null;
|
||||||
|
|
||||||
|
// Firebase has responded, but no user exists
|
||||||
|
if (!user) {
|
||||||
|
return (
|
||||||
|
<View>
|
||||||
|
<Text>Not signed in</Text>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Firebase has responded, and a user exists
|
||||||
|
return (
|
||||||
|
<View>
|
||||||
|
<Text>User signed in! {user.email}</Text>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Subscribing/Un-subscribing from listeners
|
||||||
|
|
||||||
|
When subscribing to a new listener, such as `onAuthStateChanged`, a new reference to it is made in memory which has no knowledge of the
|
||||||
|
React environment. If a component within your app mounts and subscribes, the method will still trigger even if your component unmounted.
|
||||||
|
If this happens and you're updating state, you'll get a yellow box warning.
|
||||||
|
|
||||||
|
To get around this, Firebase returns an unsubscribe function to every subscriber method, which when calls removes the subscription.
|
||||||
|
This can be easily implemented using React lifecycle methods and class properties:
|
||||||
|
|
||||||
|
```js
|
||||||
|
import React, { Component } from 'react';
|
||||||
|
import { View, Text } from 'react-native';
|
||||||
|
import firebase from 'react-native-firebase';
|
||||||
|
|
||||||
|
class App extends React.Component {
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
this.unsubscribe = null; // Set a empty class method
|
||||||
|
this.state = {
|
||||||
|
loading: false,
|
||||||
|
user: null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
// Assign the class method to the unsubscriber response
|
||||||
|
this.unsubscribe = firebase.auth().onAuthStateChanged((user) => {
|
||||||
|
// handle state changes
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillUnmount() {
|
||||||
|
// Call the unsubscriber if it has been set
|
||||||
|
if (this.unsubscribe) {
|
||||||
|
this.unsubscribe();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Further reading
|
||||||
|
|
||||||
|
The above examples just scratch the surface of whats available with Firebase auth. Firebase itself provides some in-depth documentation
|
||||||
|
on authentication and the many different implementation paths you can follow.
|
Loading…
Reference in New Issue