In this codelab, you'll learn how to use Firebase to easily create web applications by implementing and deploying a chat client using Firebase products and services.
Clone the codelab's GitHub repository from the command line:
git clone https://github.com/Zenika/firebase-web-101
Alternatively, if you do not have git installed, you can download the repository as a ZIP file.
Using your IDE, open or import the 📁 web-start
directory
from the cloned repository. This 📁 web-start
directory
contains the starting code for the codelab, which will be a fully
functional chat web app.
The application that we're going to build uses Firebase products that are available for web apps:
Some of these products need special configuration or need to be enabled using the Firebase console.
To allow users to sign in to the web app with their Google accounts, we'll use the Google sign-in method.
You'll need to enable Google sign-in:
The web app uses Cloud Firestore to save chat messages and receive new chat messages.
You'll need to enable Cloud Firestore:
Test mode ensures that we can freely write to the database during development. We'll make our database more secure later on in this codelab.
The web app uses Cloud Storage for Firebase to store, upload, and share pictures.
You'll need to enable Cloud Storage:
With the default security rules, any authenticated user can write anything to Cloud Storage. We'll make our storage more secure later in this codelab.
The Firebase command-line interface (CLI) allows you to use Firebase Hosting to serve your web app locally, as well as to deploy your web app to your Firebase project.
npm -g install firebase-tools
firebase --version
Make sure that the version of the Firebase CLI is v4.1.0 or later.
firebase login
We've set up the web app template to pull your app's configuration for Firebase Hosting from your app's local directory (the repository that you cloned earlier in the codelab). But to pull the configuration, we need to associate your app with your Firebase project.
web-start
directory.
firebase use --add
An alias is useful if you have multiple environments (production,
staging, etc). However, for this codelab, let's just use the alias
of default
.
Now that you have imported and configured your project, you are ready to run the web app for the first time.
web-start
directory, run the
following Firebase CLI command:
firebase serve --only hosting
✔ hosting: Local server: http://localhost:5000
We're using the
Firebase Hosting
emulator to serve our app locally. The web app should now be available
from
http://localhost:5000. All the files that are located under the
public
subdirectory are served.
You should see your FriendlyChat app's UI, which is not (yet!) functioning:
The app cannot do anything right now, but with your help it will soon! We've only laid out the UI for you so far.
Let's now build a realtime chat!
We need to import the Firebase SDK into the app. There are multiple ways to do this as described in our documentation. For instance, you can import the library from our CDN. Or you can install it locally using npm, then package it in your app if you're using Browserify.
Since we're using Firebase Hosting to serve our app, we're
going to import the local URLs that are in the file
index.html
(located in your
web-start/public/
directory). For this codelab, we've
already added the following lines for you at the bottom of the
index.html
file, but you can double check that they are
there.
<script src="/__/firebase/6.4.0/firebase-app.js"></script>
<script src="/__/firebase/6.4.0/firebase-auth.js"></script>
<script src="/__/firebase/6.4.0/firebase-storage.js"></script>
<script src="/__/firebase/6.4.0/firebase-messaging.js"></script>
<script src="/__/firebase/6.4.0/firebase-firestore.js"></script>
<script src="/__/firebase/6.4.0/firebase-performance.js"></script>
During this codelab, we're going to use Firebase Authentication, Cloud Firestore, Cloud Storage, Cloud Messaging, and Performance Monitoring, so we're importing all of their libraries. In your future apps, make sure that you're only importing the parts of Firebase that you need, to shorten the load time of your app.
We also need to configure the Firebase SDK to tell it which Firebase
project that we're using. Since we're using Firebase Hosting,
you can import a special script that will do this configuration for
you. Again, for this codelab, we've already added the following
line for you at the bottom of the public/index.html
file,
but double-check that it is there.
<script src="/__/firebase/init.js"></script>
This script contains your Firebase project configuration based upon
the Firebase project that you specified earlier when you ran
firebase use --add
.
Feel free to inspect the file init.js
to see what your
project configuration looks like. To do this, open
http://localhost:5000/__/firebase/init.js
in your browser. You should see something that looks like the
following:
if (typeof firebase === 'undefined') throw new Error('hosting/init-error: Firebase SDK not detected. You must include it before /__/firebase/init.js');
firebase.initializeApp({
"apiKey": "qwertyuiop_asdfghjklzxcvbnm1234568_90",
"databaseURL": "https://friendlychat-1234.firebaseio.com",
"storageBucket": "friendlychat-1234.appspot.com",
"authDomain": "friendlychat-1234.firebaseapp.com",
"messagingSenderId": "1234567890",
"projectId": "friendlychat-1234",
"appId": "1:1234567890:web:123456abcdef"
});
The Firebase SDK should now be ready to use since it's imported
and initialized in index.html
. We're now going to
implement user sign-in using
Firebase Authentication.
In the app, when a user clicks the
Sign in with Google button, the
signIn
function is triggered. (We already set that up for
you!) For this codelab, we want to authorize Firebase to use Google as
the identity provider. We'll use a popup, but
several other methods
are available from Firebase.
web-start
directory, in the subdirectory
public/scripts/
, open main.js
.
signIn
.// Signs-in Friendly Chat.
function signIn() {
// Sign into Firebase using popup auth & Google as the identity provider.
var provider = new firebase.auth.GoogleAuthProvider();
firebase.auth().signInWithPopup(provider);
}
The signOut
function is triggered when the user clicks
the Sign out button.
public/scripts/main.js
.signOut
.// Signs-out of Friendly Chat.
function signOut() {
// Sign out of Firebase.
firebase.auth().signOut();
}
To update our UI accordingly, we need a way to check if the user is signed in or signed out. With Firebase Authentication, you can register an observer on the authentication state that will be triggered each time the authentication state changes.
public/scripts/main.js
.initFirebaseAuth
.// Initiate Firebase Auth.
function initFirebaseAuth() {
// Listen to auth state changes.
firebase.auth().onAuthStateChanged(authStateObserver);
}
The code above registers the function
authStateObserver
as the authentication state observer.
It will trigger each time the authentication state changes (when the
user signs in or signs out). It's at this point that we'll
update the UI to display or hide the sign-in button, the sign-out
button, the signed-in user's profile picture, and so on. All of
these UI parts have already been implemented.
We want to display the signed-in user's profile picture and user
name in the top bar of our app. In Firebase, the signed-in user's
data is always available in the
firebase.auth().currentUser
object. Earlier, we set up
the authStateObserver
function to trigger when the user
signs in so that our UI updates accordingly. It will call
getProfilePicUrl
and getUserName
when
triggered.
public/scripts/main.js
.getProfilePicUrl
and
getUserName
.
// Returns the signed-in user's profile pic URL.
function getProfilePicUrl() {
return firebase.auth().currentUser.photoURL || '/images/profile_placeholder.png';
}
// Returns the signed-in user's display name.
function getUserName() {
return firebase.auth().currentUser.displayName;
}
We display an error message if the user tries to send messages when the user isn't signed in. (You can try it, though!) So, we need to detect if the user is actually signed in.
public/scripts/main.js
.isUserSignedIn
.// Returns true if a user is signed-in.
function isUserSignedIn() {
return !!firebase.auth().currentUser;
}
firebase serve
on the command line to
start serving the app from
http://localhost:5000, and then open it in your browser.
auth/operation-not-allowed
, check to make sure that you
enabled Google Sign-in as an authentication provider in the Firebase
console
In this section, we'll write some data to Cloud Firestore so that we can populate the app's UI. This can be done manually with the Firebase console, but we'll do it in the app itself to demonstrate a basic Cloud Firestore write.
Cloud Firestore data is split into collections, documents, fields, and
subcollections. We will store each message of the chat as a document
in a top-level collection called messages
.
To store the chat messages that are written by users, we'll use Cloud Firestore.
In this section, you'll add the functionality for users to write
new messages to your database. A user clicking the
SEND button will trigger the code snippet below. It
adds a message object with the contents of the message fields to your
Cloud Firestore instance in the messages
collection. The
add()
method adds a new document with an automatically
generated ID to the collection.
public/scripts/main.js
.saveMessage
.// Saves a new message to your Cloud Firestore database.
function saveMessage(messageText) {
// Add a new message entry to the database.
return firebase.firestore().collection('messages').add({
name: getUserName(),
text: messageText,
profilePicUrl: getProfilePicUrl(),
timestamp: firebase.firestore.FieldValue.serverTimestamp()
}).catch(function(error) {
console.error('Error writing new message to database', error);
});
}
firebase serve
on the command line to
start serving the app from
http://localhost:5000, and then open it in your browser.
To read messages in the app, we'll need to add listeners that trigger when data changes and then create a UI element that shows new messages.
We'll add code that listens for newly added messages from the app. In this code, we'll register the listener that listens for changes made to the data. We'll only display the last 12 messages of the chat to avoid displaying a very long history upon loading.
public/scripts/main.js
.loadMessages
.// Loads chat messages history and listens for upcoming ones.
function loadMessages() {
// Create the query to load the last 12 messages and listen for new ones.
var query = firebase.firestore()
.collection('messages')
.orderBy('timestamp', 'desc')
.limit(12);
// Start listening to the query.
query.onSnapshot(function(snapshot) {
snapshot.docChanges().forEach(function(change) {
if (change.type === 'removed') {
deleteMessage(change.doc.id);
} else {
var message = change.doc.data();
displayMessage(change.doc.id, message.timestamp, message.name,
message.text, message.profilePicUrl, message.imageUrl);
}
});
});
}
To listen to messages in the database, we create a query on a
collection by using the .collection
function to specify
which collection the data that we want to listen to is in. In the code
above, we're listening to the changes within the
messages
collection, which is where the chat messages are
stored. We're also applying a limit by only listening to the last
12 messages using .limit(12)
and ordering the messages by
date using
.orderBy('timestamp', 'desc')
to get the
12 newest messages.
The .onSnapshot
function takes one parameter: a callback
function. The callback function will be triggered when there are any
changes to documents that match the query. This could be if a message
gets deleted, modified, or added. You can read more about this in the
Cloud Firestore documentation.
firebase serve
on the command line to
start serving the app from
http://localhost:5000, and then open it in your browser.
Congratulations! You are reading Cloud Firestore documents in your app!
Cloud Firestore uses a specific rules language to define access rights, security, and data validations.
When setting up the Firebase project at the beginning of this codelab, we chose to use "Test mode" default security rules so that we didn't restrict access to the datastore. In the Firebase console, in the Database section's Rules tab, you can view and modify these rules.
Right now, you should see the default rules, which do not restrict access to the datastore. This means that any user can read and write to any collections in your datastore.
service cloud.firestore {
match /databases/{database}/documents {
match /{document=**} {
allow read, write;
}
}
}
We'll update the rules to restrict things by using the following rules:
service cloud.firestore {
match /databases/{database}/documents {
// Messages:
// - Anyone can read.
// - Authenticated users can add and edit messages.
// - Validation: Check name is same as auth token and text length below 300 char or that imageUrl is a URL.
// - Deletes are not allowed.
match /messages/{messageId} {
allow read;
allow create, update: if request.auth != null
&& request.resource.data.name == request.auth.token.name
&& (request.resource.data.text is string
&& request.resource.data.text.size() <= 300
|| request.resource.data.imageUrl is string
&& request.resource.data.imageUrl.matches('https?://.*'));
allow delete: if false;
}
// FCM Tokens:
// - Anyone can write their token.
// - Reading list of tokens is not allowed.
match /fcmTokens/{token} {
allow read: if false;
allow write;
}
}
}
There are two ways to edit your database security rules, either in the Firebase console or from a local rules file deployed using the Firebase CLI.
To update security rules in the Firebase console:
To update security rules from a local file:
web-start
directory, open
firestore.rules
.
web-start
directory, open
firebase.json
.
firestore.rules
attribute pointing to
firestore.rules
, as shown below. (The
hosting
attribute should already be in the file.)
{
// Add this!
"firestore": {
"rules": "firestore.rules"
},
"hosting": {
"public": "./public"
}
}
firebase deploy --only firestore
=== Deploying to 'friendlychat-1234'...
i deploying firestore
i firestore: checking firestore.rules for compilation errors...
✔ firestore: rules file firestore.rules compiled successfully
i firestore: uploading rules firestore.rules...
✔ firestore: released rules firestore.rules to cloud.firestore
✔ Deploy complete!
Project Console: https://console.firebase.google.com/project/friendlychat-1234/overview
Cloud Storage for Firebase uses a specific rules language to define access rights, security, and data validations.
When setting up the Firebase project at the beginning of this codelab, we chose to use the default Cloud Storage security rule that only allows authenticated users to use Cloud Storage. In the Firebase console, in the Storage section's Rules tab, you can view and modify rules. You should see the default rule which allows any signed-in user to read and write any files in your storage bucket.
service firebase.storage {
match /b/{bucket}/o {
match /{allPaths=**} {
allow read, write: if request.auth != null;
}
}
}
We'll update the rules to do the following:
This can be implemented using the following rules:
// Returns true if the uploaded file is an image and its size is below the given number of MB.
function isImageBelowMaxSize(maxSizeMB) {
return request.resource.size < maxSizeMB * 1024 * 1024
&& request.resource.contentType.matches('image/.*');
}
service firebase.storage {
match /b/{bucket}/o {
match /{userId}/{messageId}/{fileName} {
allow write: if request.auth != null && request.auth.uid == userId && isImageBelowMaxSize(5);
allow read;
}
}
}
There are two ways to edit your storage security rules: either in the Firebase console or from a local rules file deployed using the Firebase CLI.
To update security rules in the Firebase console:
To update security rules from a local file:
web-start
directory, open
storage.rules
.
web-start
directory, open
firebase.json
.
storage.rules
attribute pointing to the
storage.rules
file, as shown below. (The
hosting
and database
attribute should
already be in the file.)
{
// If you went through the "Cloud Firestore Security Rules" step.
"firestore": {
"rules": "firestore.rules"
},
// Add this!
"storage": {
"rules": "storage.rules"
},
"hosting": {
"public": "./public"
}
}
firebase deploy --only storage
=== Deploying to 'friendlychat-1234'...
i deploying storage
i storage: checking storage.rules for compilation errors...
✔ storage: rules file storage.rules compiled successfully
i storage: uploading rules storage.rules...
✔ storage: released rules storage.rules to firebase.storage/friendlychat-1234.appspot.com
✔ Deploy complete!
Project Console: https://console.firebase.google.com/project/friendlychat-1234/overview
Firebase offers a
hosting service
to serve your assets and web apps. You can deploy your files to
Firebase Hosting using the Firebase CLI. Before deploying, you need to
specify in your firebase.json
file which local files
should be deployed. For this codelab, we've already done this for
you because this step was required to serve our files during this
codelab. The hosting settings are specified under the
hosting
attribute:
{
// If you went through the "Cloud Firestore Security Rules" step.
"firestore": {
"rules": "firestore.rules"
},
// If you went through the "Storage Security Rules" step.
"storage": {
"rules": "storage.rules"
},
"hosting": {
"public": "./public"
}
}
These settings tell the CLI that we want to deploy all files in the
./public
directory (
"public": "./public"
).
web-start
directory.
firebase deploy --except functions
=== Deploying to 'friendlychat-1234'...
i deploying firestore, storage, hosting
i storage: checking storage.rules for compilation errors...
✔ storage: rules file storage.rules compiled successfully
i firestore: checking firestore.rules for compilation errors...
✔ firestore: rules file firestore.rules compiled successfully
i storage: uploading rules storage.rules...
i firestore: uploading rules firestore.rules...
i hosting[friendlychat-1234]: beginning deploy...
i hosting[friendlychat-1234]: found 8 files in ./public
✔ hosting[friendlychat-1234]: file upload complete
✔ storage: released rules storage.rules to firebase.storage/friendlychat-1234.appspot.com
✔ firestore: released rules firestore.rules to cloud.firestore
i hosting[friendlychat-1234]: finalizing version...
✔ hosting[friendlychat-1234]: version finalized
i hosting[friendlychat-1234]: releasing new version...
✔ hosting[friendlychat-1234]: release complete
✔ Deploy complete!
Project Console: https://console.firebase.google.com/project/friendlychat-1234/overview
Hosting URL: https://friendlychat-1234.firebaseapp.com
https://<firebase-projectId>.firebaseapp.com
https://<firebase-projectId>.web.app
.
Alternatively, you can run firebase open hosting:site
in
the command line.
Visit the documentation to learn more about how Firebase Hosting works.
Go to your project's Firebase console Hosting section to view useful hosting information and tools, including the history of your deploys, the functionality to roll back to previous versions of your app, and the workflow to set up a custom domain.
Cloud Functions allows you to easily have code that runs in the Cloud without having to setup a server. We'll be showing you how to build functions that react to Firebase Auth, Cloud Storage and Firebase Realtime Database events. Let's start with Auth.
When using the Firebase SDK for Cloud Functions, your Functions code
will live under the functions
directory (by default).
Your Functions code is also a
Node.js app and
therefore needs a package.json
that gives some
information about your app and lists dependencies.
To make it easier for you we've already created the
functions/index.js
file where your code will go. Feel
free to inspect this file before moving forward.
cd functions
ls
If you are not familiar with Node.js it will help to learn more about it before continuing the codelab.
The package.json file already lists two required dependencies: the
Firebase SDK for Cloud Functions
and the
Firebase Admin SDK. To install them locally run npm install
from the
functions
folder:
npm install
Let's now have a look at the index.js
file:
/**
* Copyright 2017 Google Inc. All Rights Reserved.
* ...
*/
// TODO(DEVELOPER): Import the Cloud Functions for Firebase and the Firebase Admin modules here.
// TODO(DEVELOPER): Write the addWelcomeMessage Function here.
// TODO(DEVELOPER): Write the blurImages Function here.
// TODO(DEVELOPER): Write the sendNotification Function here.
We'll first import the required modules and then write three Functions in place of the TODOs. First let's import the required Node modules.
Two modules will be required during this codelab, the
firebase-functions
module allows us to write the Cloud
Functions trigger rules, while the firebase-admin
module
allows us to use the Firebase platform on a server with admin access,
for instance to write to the Cloud Firestore or send FCM
notifications.
In the index.js
file, replace the first
TODO
with the following:
/**
* Copyright 2017 Google Inc. All Rights Reserved.
* ...
*/
// Import the Firebase SDK for Google Cloud Functions.
const functions = require('firebase-functions');
// Import and initialize the Firebase Admin SDK.
const admin = require('firebase-admin');
admin.initializeApp();
// TODO(DEVELOPER): Write the addWelcomeMessage Function here.
// TODO(DEVELOPER): Write the blurImages Function here.
// TODO(DEVELOPER): Write the sendNotification Function here.
The Firebase Admin SDK can be configured automatically when deployed
on a Cloud Functions environment or other Google Cloud Platform
containers. This is what we do above when calling
admin.initializeApp();
Now let's add a Function that runs when a user signs in for the first time in your chat app and we'll add a chat message to welcome the user.
Messages posted to the FriendlyChat chat feed are stored in the Cloud Firestore. Let's have a look at the data structure we use for a message. To do this, post a new message to the chat that reads "Hello World":
This should appear as:
In your Firebase app console click on Database under the Develop section. You should see the messages collection and one document containing the message that you wrote:
As you can see, chat messages are stored in the Cloud Firestore as a
document with the name
, profilePicUrl
,
text
and timestamp
attributes added to the
messages
collection.
The first Cloud Function adds a message that welcomes
new users into the chat. For this we can use the
trigger functions.auth().onCreate
which runs the function
every time a user signs-in for the first time in your Firebase app.
Add the addWelcomeMessages
function into your
index.js
file:
// Adds a message that welcomes new users into the chat.
exports.addWelcomeMessages = functions.auth.user().onCreate(async (user) => {
console.log('A new user signed in for the first time.');
const fullName = user.displayName || 'Anonymous';
// Saves the new welcome message into the database
// which then displays it in the FriendlyChat clients.
await admin.firestore().collection('messages').add({
name: 'Firebase Bot',
profilePicUrl: '/images/firebase-logo.png', // Firebase logo
text: `${fullName} signed in for the first time! Welcome!`,
timestamp: admin.firestore.FieldValue.serverTimestamp(),
});
console.log('Welcome message written to database.');
});
Adding this function to the special exports
object is
Node's way of making the function accessible outside of the
current file and is required for Cloud Functions.
In the function above we are adding a new welcome message posted by
"Firebase Bot" to the list of chat messages. We are doing this
by using the
add
method on the messages
collection in the Cloud Firestore
which is where the messages of the chat are stored.
Since this is an asynchronous operation, we need to return the Promise indicating when the Cloud Firestore write has finished, so that Functions doesn't exit the execution too early.
The Function will only be active after you've deployed it. On the
command line run firebase deploy --only functions
:
firebase deploy --only functions
This is the console output you should see:
i deploying functions
i functions: ensuring necessary APIs are enabled...
⚠ functions: missing necessary APIs. Enabling now...
i env: ensuring necessary APIs are enabled...
⚠ env: missing necessary APIs. Enabling now...
i functions: waiting for APIs to activate...
i env: waiting for APIs to activate...
✔ env: all necessary APIs are enabled
✔ functions: all necessary APIs are enabled
i functions: preparing functions directory for uploading...
i functions: packaged functions (X.XX KB) for uploading
✔ functions: functions folder uploaded successfully
i starting release process (may take several minutes)...
i functions: creating function addWelcomeMessages...
✔ functions[addWelcomeMessages]: Successful create operation.
✔ functions: all functions deployed successfully!
✔ Deploy complete!
Project Console: https://console.firebase.google.com/project/friendlypchat-1234/overview
Once the function has deployed successfully you'll need to have a user that signs in for the first time.
https://<project-id>.firebaseapp.com
).
To deploy the whole project (static files, functions, rules...), go back to the
web-start
folder and run
firebase deploy
:
firebase deploy
This is the console output you should see:
i deploying storage, firestore, functions, hosting
i storage: checking storage.rules for compilation errors...
✔ storage: rules file storage.rules compiled successfully
i firestore: checking firestore.rules for compilation errors...
✔ firestore: rules file firestore.rules compiled successfully
i functions: ensuring necessary APIs are enabled...
✔ functions: all necessary APIs are enabled
i storage: uploading rules storage.rules...
i firestore: uploading rules firestore.rules...
i functions: preparing functions directory for uploading...
i functions: packaged functions (36.16 KB) for uploading
✔ functions: functions folder uploaded successfully
i hosting[friendlychat-1234]: beginning deploy...
i hosting[friendlychat-1234]: found 7 files in ./public
✔ hosting[friendlychat-1234]: file upload complete
✔ storage: released rules storage.rules to firebase.storage/friendlychat-1234.appspot.com
✔ firestore: released rules firestore.rules to cloud.firestore
i functions: updating Node.js 8 function addWelcomeMessages(us-central1)...
✔ functions[addWelcomeMessages(us-central1)]: Successful update operation.
i hosting[friendlychat-1234]: finalizing version...
✔ hosting[friendlychat-1234]: version finalized
i hosting[friendlychat-1234]: releasing new version...
✔ hosting[friendlychat-1234]: release complete
✔ Deploy complete!
Project Console: https://console.firebase.google.com/project/friendlychat-1234/overview
Hosting URL: https://friendlychat-1234.firebaseapp.com
The last line should display the Hosting URL. The web app should now be served from this URL which should be of the form https://<project-id>.firebaseapp.com. Open it. You should see a chat app's functioning UI.
Sign-in to the app by using the SIGN-IN WITH GOOGLE button and feel free to add some messages:
We'll now add a feature that shares images.
While the Cloud Firestore is good for storing structured data, Cloud Storage is better suited for storing files. Cloud Storage for Firebase is a file/blob storage service, and we'll use it to store any images that a user shares using our app.
For this codelab, we've already added for you a button that
triggers a file picker dialog. After selecting a file, the
saveImageMessage
function is called, and you can get a
reference to the selected file. The
saveImageMessage
function accomplishes the following:
/<uid>/<messageId>/<file_name>
Now you'll add the functionality to sned an image:
public/scripts/main.js
.saveImageMessage
.// Saves a new message containing an image in Firebase.
// This first saves the image in Firebase storage.
function saveImageMessage(file) {
// 1 - We add a message with a loading icon that will get updated with the shared image.
firebase.firestore().collection('messages').add({
name: getUserName(),
imageUrl: LOADING_IMAGE_URL,
profilePicUrl: getProfilePicUrl(),
timestamp: firebase.firestore.FieldValue.serverTimestamp()
}).then(function(messageRef) {
// 2 - Upload the image to Cloud Storage.
var filePath = firebase.auth().currentUser.uid + '/' + messageRef.id + '/' + file.name;
return firebase.storage().ref(filePath).put(file).then(function(fileSnapshot) {
// 3 - Generate a public URL for the file.
return fileSnapshot.ref.getDownloadURL().then((url) => {
// 4 - Update the chat message placeholder with the image's URL.
return messageRef.update({
imageUrl: url,
storageUri: fileSnapshot.metadata.fullPath
});
});
});
}).catch(function(error) {
console.error('There was an error uploading a file to Cloud Storage:', error);
});
}
firebase serve --only hosting
on the
command line to start serving the app from
http://localhost:5000, and then open it in your browser.
If you try adding an image while not signed in, you should see a Toast notification telling you that you must sign in to add images.
We'll now add support for browser notifications. The app will notify users when new messages are posted in the chat. Firebase Cloud Messaging (FCM) is a cross-platform messaging solution that lets you reliably deliver messages and notifications at no cost.
In the
web app manifest, you need to specify the gcm_sender_id
, which is a
hard-coded value indicating that FCM is authorized to send messages to
this app.
web-start
directory, in the
public
directory, open manifest.json
.
gcm_sender_id
attribute exactly as shown below. Do not
change the value from what's shown below.
{
"name": "Friendly Chat",
"short_name": "Friendly Chat",
"start_url": "/index.html",
"display": "standalone",
"orientation": "portrait",
"gcm_sender_id": "103953800507"
}
The web app needs a service worker that will receive and display web notifications.
web-start
directory, in the
public
directory, create a new file named
firebase-messaging-sw.js
.
importScripts('/__/firebase/6.0.4/firebase-app.js');
importScripts('/__/firebase/6.0.4/firebase-messaging.js');
importScripts('/__/firebase/init.js');
firebase.messaging();
The service worker simply needs to load and initialize the Firebase Cloud Messaging SDK, which will take care of displaying notifications.
When notifications have been enabled on a device or browser, you'll be given a device token. This device token is what we use to send a notification to a particular device or particular browser.
When the user signs-in, we call the
saveMessagingDeviceToken
function. That's where
we'll get the FCM device token from the browser
and save it to Cloud Firestore.
public/scripts/main.js
.saveMessagingDeviceToken
.// Saves the messaging device token to the datastore.
function saveMessagingDeviceToken() {
firebase.messaging().getToken().then(function(currentToken) {
if (currentToken) {
console.log('Got FCM device token:', currentToken);
// Saving the Device Token to the datastore.
firebase.firestore().collection('fcmTokens').doc(currentToken)
.set({uid: firebase.auth().currentUser.uid});
} else {
// Need to request permissions to show notifications.
requestNotificationsPermissions();
}
}).catch(function(error){
console.error('Unable to get messaging token.', error);
});
}
However, this code won't work initially. For your app to be able to retrieve the device token, the user needs to grant your app permission to show notifications (next step of the codelab).
When the user has not yet granted your app permission to show
notifications, you won't be given a device token. In this case, we
call the firebase.messaging().requestPermission()
method,
which will display a browser dialog asking for this permission (in supported browsers).
public/scripts/main.js
.requestNotificationsPermissions
.
// Requests permission to show notifications.
function requestNotificationsPermissions() {
console.log('Requesting notifications permission...');
firebase.messaging().requestPermission().then(function() {
// Notification permission granted.
saveMessagingDeviceToken();
}).catch(function(error) {
console.error('Unable to get permission to notify.', error);
});
}
firebase serve
on the command line to
start serving the app from
http://localhost:5000, and then open it in your browser.
Got FCM device token:
cWL6w:APA91bHP...4jDPL_A-wPP06GJp1OuekTaTZI5K2Tu
Now that you have your device token, you can send a notification.
To send a notification, you'll need to send the following HTTP request:
POST /fcm/send HTTP/1.1
Host: fcm.googleapis.com
Content-Type: application/json
Authorization: key=YOUR_SERVER_KEY
{
"notification": {
"title": "New chat message!",
"body": "There is a new message in FriendlyChat",
"icon": "/images/profile_placeholder.png",
"click_action": "http://localhost:5000"
},
"to":"YOUR_DEVICE_TOKEN"
}
curl -H "Content-Type: application/json" \
-H "Authorization: key=YOUR_SERVER_KEY" \
-d '{
"notification": {
"title": "New chat message!",
"body": "There is a new message in FriendlyChat",
"icon": "/images/profile_placeholder.png",
"click_action": "http://localhost:5000"
},
"to": "YOUR_DEVICE_TOKEN"
}' \
https://fcm.googleapis.com/fcm/send
Note that the notification will only appear if the FriendlyChat app is in the background. You must navigate away or display another tab for the notification to be displayed. When the app is in the foreground, there is a way to catch the messages sent by FCM.
If your app is in the background, a notification should appear in your browser, as in this example:
In this section you will add a Cloud Function that sends notifications to participants of the chat when a new message is posted.
Using
Firebase Cloud Messaging
(FCM) you can send notifications to your users in a cross platform and
reliable way. To send a notification to a user you need their FCM
device token. The chat web app that we are using already collects
device tokens from users when they open the app for the first time on
a new browser or device. These tokens are stored in Cloud Firestore in
the fcmTokens
collection.
To detect when new messages are posted you'll be using the
functions.firestore.document().onCreate
Cloud Functions
trigger which runs your code when a new object is created at a given
path of the Cloud Firestore. Add the
sendNotifications
function into your
index.js
file:
// Sends a notifications to all users when a new message is posted.
exports.sendNotifications = functions.firestore.document('messages/{messageId}').onCreate(
async (snapshot) => {
// Notification details.
const text = snapshot.data().text;
const payload = {
notification: {
title: `${snapshot.data().name} posted ${text ? 'a message' : 'an image'}`,
body: text ? (text.length <= 100 ? text : text.substring(0, 97) + '...') : '',
icon: snapshot.data().profilePicUrl || '/images/profile_placeholder.png',
click_action: `https://${process.env.GCLOUD_PROJECT}.firebaseapp.com`,
}
};
// Get the list of device tokens.
const allTokens = await admin.firestore().collection('fcmTokens').get();
const tokens = [];
allTokens.forEach((tokenDoc) => {
tokens.push(tokenDoc.id);
});
if (tokens.length > 0) {
// Send notifications to all tokens.
const response = await admin.messaging().sendToDevice(tokens, payload);
await cleanupTokens(response, tokens);
console.log('Notifications have been sent and tokens cleaned up.');
}
});
In the Function above we are gathering all users' device tokens
from the Cloud Firestore Database and sending a notification to each
of these using the
admin.messaging().sendToDevice
function.
Lastly we want to remove the tokens that are not valid anymore. This
happens when the token that we once got from the user is not being
used by the browser or device anymore. For instance, this happens if
the user has revoked the notification permission for his browser
session. To do this add the following
cleanupTokens
function in your
index.js
file:
// Cleans up the tokens that are no longer valid.
function cleanupTokens(response, tokens) {
// For each notification we check if there was an error.
const tokensDelete = [];
response.results.forEach((result, index) => {
const error = result.error;
if (error) {
console.error('Failure sending notification to', tokens[index], error);
// Cleanup the tokens who are not registered anymore.
if (error.code === 'messaging/invalid-registration-token' ||
error.code === 'messaging/registration-token-not-registered') {
const deleteTask = admin.firestore().collection('messages').doc(tokens[index]).delete();
tokensDelete.push(deleteTask);
}
}
});
return Promise.all(tokensDelete);
}
The Function will only be active after you've deployed it. On the
command line run firebase deploy --only functions
:
firebase deploy --only functions
This is the console output you should see:
i deploying functions
i functions: ensuring necessary APIs are enabled...
✔ functions: all necessary APIs are enabled
i functions: preparing functions directory for uploading...
i functions: packaged functions (X.XX KB) for uploading
✔ functions: functions folder uploaded successfully
i starting release process (may take several minutes)...
i functions: updating function addWelcomeMessages...
i functions: updating function blurOffensiveImages...
i functions: creating function sendNotifications...
✔ functions[addWelcomeMessages]: Successful update operation.
✔ functions[blurOffensiveImages]: Successful updating operation.
✔ functions[sendNotifications]: Successful create operation.
✔ functions: all functions deployed successfully!
✔ Deploy complete!
Project Console: https://console.firebase.google.com/project/friendlychat-1234/overview
https://<project-id>.firebaseapp.com
).
You've used Firebase to build a real-time chat web application!
In this section you will add a firebase extensions for automatically translate all texts.
Using Firebase Extensions With firebase extensions, you can install pre-packaged solutions for help you
We wan't to add a new fonctionnality for translate all messages into multiple languages and save it in the Cloud Firestore.
Let's check the catalog of available extensions on the Firebase website.
Look at that! There's an extension called "Translate Text". That looks promising.
Let's use this extension in your app!
Collection path
field, enter the following:messages
Input field name
field, enter the following:text
Users can upload all type of images in the chat, and it is always important to moderate offensive images, especially in public social platforms. In FriendlyChat the images that are being published to the chat are stored into Google Cloud Storage.
With Cloud Functions, you can detect new image uploads using the
functions.storage().onFinalize
trigger. This will run
every time a new file is uploaded or modified in Cloud Storage.
To moderate images we'll go through the following process:
Since we'll be using the Google Cloud Vision API in this function, you must enable the API on your firebase project. Follow this link, select your Firebase project and enable the API:
To moderate the images we'll need a few Node.js packages:
To install these two packages into your Cloud Functions app, run the
following npm install --save
command. Make sure that you
do this from the functions
directory.
npm install --save @google-cloud/vision@0.12.0 child-process-promise@2.2.1
This will install the two packages locally and add them as declared
dependencies in your package.json
file.
To import the two dependencies that were installed and some Node.js
core modules (path
, os
and fs
)
that we'll need in this section add the following lines to the top
of your index.js
file:
const Vision = require('@google-cloud/vision');
const vision = new Vision();
const spawn = require('child-process-promise').spawn;
const path = require('path');
const os = require('os');
const fs = require('fs');
Since your function will run inside a Google Cloud environment, there is no need to configure the Cloud Storage and Cloud Vision libraries: they will be configured automatically to use your project.
You'll be using the functions.storage.onChange
Cloud
Functions trigger which runs your code as soon as a file or folder is
created or modified in a Cloud Storage bucket. Add the
blurOffensiveImages
Function into your
index.js
file:
// Checks if uploaded images are flagged as Adult or Violence and if so blurs them.
exports.blurOffensiveImages = functions.runWith({memory: '2GB'}).storage.object().onFinalize(
async (object) => {
const image = {
source: {imageUri: `gs://${object.bucket}/${object.name}`},
};
// Check the image content using the Cloud Vision API.
const batchAnnotateImagesResponse = await vision.safeSearchDetection(image);
const safeSearchResult = batchAnnotateImagesResponse[0].safeSearchAnnotation;
const Likelihood = Vision.types.Likelihood;
if (Likelihood[safeSearchResult.adult] >= Likelihood.LIKELY ||
Likelihood[safeSearchResult.violence] >= Likelihood.LIKELY) {
console.log('The image', object.name, 'has been detected as inappropriate.');
return blurImage(object.name);
}
console.log('The image', object.name, 'has been detected as OK.');
});
Note that we added some configuration of the Cloud Functions instance
that will run the function, with
.runWith({memory: '2GB'})
we're requesting
that the instance gets 2GB of memory rather than the default, this
will help as this function is memory intensive.
When the function is triggered, the image is ran through the Cloud
Vision API to detect if it is flagged as adult or violent. If the
image is detected as inappropriate based on these criteria we're
blurring the image which is done in the
blurImage
function which we'll see next.
Add the following blurImage
function in your
index.js
file:
// Blurs the given image located in the given bucket using ImageMagick.
async function blurImage(filePath) {
const tempLocalFile = path.join(os.tmpdir(), path.basename(filePath));
const messageId = filePath.split(path.sep)[1];
const bucket = admin.storage().bucket();
// Download file from bucket.
await bucket.file(filePath).download({destination: tempLocalFile});
console.log('Image has been downloaded to', tempLocalFile);
// Blur the image using ImageMagick.
await spawn('convert', [tempLocalFile, '-channel', 'RGBA', '-blur', '0x24', tempLocalFile]);
console.log('Image has been blurred');
// Uploading the Blurred image back into the bucket.
await bucket.upload(tempLocalFile, {destination: filePath});
console.log('Blurred image has been uploaded to', filePath);
// Deleting the local file to free up disk space.
fs.unlinkSync(tempLocalFile);
console.log('Deleted local file.');
// Indicate that the message has been moderated.
await admin.firestore().collection('messages').doc(messageId).update({moderated: true});
console.log('Marked the image as moderated in the database.');
}
In the above function the image binary is downloaded from Cloud
Storage. Then the image is blurred using ImageMagick's
convert
tool and the blurred version is re-uploaded on
the Storage Bucket. Then we delete the file on the Cloud Functions
instance to free up some disk space, we do this because the same Cloud
Functions instance can get re-used and if files are not cleaned up it
could run out of disk. Finally we add a boolean to the chat message
indicating the image was moderated, this will trigger a refresh of the
message on the client.
The Function will only be active after you've deployed it. On the
command line run firebase deploy --only functions
:
firebase deploy --only functions
This is the console output you should see:
i deploying functions
i functions: ensuring necessary APIs are enabled...
✔ functions: all necessary APIs are enabled
i functions: preparing functions directory for uploading...
i functions: packaged functions (X.XX KB) for uploading
✔ functions: functions folder uploaded successfully
i starting release process (may take several minutes)...
i functions: updating function addWelcomeMessages...
i functions: creating function blurOffensiveImages...
✔ functions[addWelcomeMessages]: Successful update operation.
✔ functions[blurOffensiveImages]: Successful create operation.
✔ functions: all functions deployed successfully!
✔ Deploy complete!
Project Console: https://console.firebase.google.com/project/friendlychat-1234/overview
Once the function has deployed successfully:
https://<project-id>.firebaseapp.com
).