[MUSIC PLAYING] ANDREA WU: Hello, welcome back
to the fourth and final video of the series where we
are building and managing an expense tracker web app
using Firebase, React, and Next. If you didn't watch the
other videos, don't worry. You can jump straight
into this one without needing to
go back to them. As a recap of the
video series so far, we built a MVP for
our expense tracker in the first video
enabling our users to create an account and login
as well as add, view, edit, and delete expenses. We did so using Firebase
authentication, storage, and Firestore. And after building
out all of that, we deployed our great web
app to Firebase Hosting, so we can share it with
our friends to use. In the second video, we
improved upon the MVP and implemented optical
character recognition, also known as OCR. This enabled users to just
upload a receipt image without needing to type in
every single field themselves for every single expense
they want to add, since that can become
really tedious. After uploading the
receipt, they'll just need to confirm the
contents of the receipt and edit if necessary. We did this using Cloud
Functions and Google's Vision API, which automatically extract
a text from the uploaded image. In the third video,
we talked about how to hide this new OCR feature
behind a feature flag during development
then gradually roll out the feature to users after
we're done developing it. We used the Remote Config to do
feature flagging and Analytics to help us understand how users
were experiencing the rollout. So what are we
going to do today? Well, since we have this app
out and about for a while now, we might want to think about how
to retain the users we already have and continue
to engage them. One idea that comes to
mind is sending users a notification once
a receipt is finished processing to remind them to
confirm the receipt contents. And another idea is to
send users a reminder once a week to upload any
new expenses that they may have incurred in the last week. [MUSIC PLAYING] To implement both
of these ideas, we're going to use
Firebase Cloud Messaging. What is Firebase Cloud
Messaging, or FCM for short? Well, it's a free
service that enables developers to send messages
between server and client apps. And these messages can be
addressed to single devices, groups of devices, or topics. It can be used to deliver
notifications and messages to your users on Android,
iOS, and the web, and you don't need
to add any logic to keep track of
what kind of devices your customers are using
or set up different service calls to different devices. Firebase will take care
of all of that for you. FCM is powerful
and scalable as it delivers hundreds of
billions of messages per day for more than a
quarter-million applications, with most of them being
delivered in less than 200 milliseconds. Let's see a quick
overview of how FCM works before we dive into coding. When a device first
registers with the service, you get back a token, which can
be used to uniquely identify this device to the service. Your device can also
ask to subscribe to topics, which can
really be any arbitrary string you specify. And the service will keep
track of all of that for you. Behind the scenes,
Firebase will also associate this user with
a unique instance IDs from Analytics for Firebase. So you can send notifications
with this user based on certain Analytics
data while still doing so in a privacy-respectful way. To send notifications,
you simply tell FCM, either using the
Firebase Console, Cloud Functions, or your own servers,
what message you want to send and who you want to send it to. You can target an individual
device using its FCM token, a group of devices
that have all subscribed to the same topic, or you can
make use of Firebase Analytics to send messages to
groups of users who have some exhibited behavior. All right, so now that we have
a high-level overview of how FCM works, let's
see how we're going to implement it for our app. [MUSIC PLAYING] Before we do that, we'll have
to do some project and code setup also to make sure the
app is working and running before we try to do anything. Let's clone the
GitHub repo, which is linked in the description. Even if it came from
any of the other videos, you should clone the repo
because the starter code for this video is slightly
different from the other videos code. We'll also need a
Firebase project. If you don't already
have a Firebase project from the video
series so far, feel free to follow the link in the
description on how to do so. If you do already
have a project, please go to Firebase Console,
copy our project configuration by going to Project
Settings, and paste that into the Firebase Config
variable in Firebase.js. Next, copy the storage
bucket URL and open storage.js to set the
BUCKET_URL variable as what you just copied. This is going to
make sure we upload images to the correct storage
bucket for your project. We'll also have to do one more
step in Firebase Console, which is to upgrade to the Blaze
plan if you haven't already, as this code uses
Firebase Functions. And you can follow the
link in the description to upgrade your plan. After doing all
of that, let's do npm install on the
command line to install all of the dependencies. Let's then go into
our functions folder and NPM install
everything there as well. While it's doing
that, we can run Firebase Login, which will
have us log in via the browser. Choose the appropriate email
address, the one with which you created your Firebase project,
and allow Firebase CLI to access your Google account. Upon success, the UI
will reflect that and so will the command line. Next, let's deploy the function
we have in our functions folder by doing Firebase
deploy --only functions, and this might take a minute. Let's wait for it to finish. After it does, we can then
run the app using npm run dev. And open the web app just
to check it's working. Everything seems to
be running just fine, so let's get started with FCM. [MUSIC PLAYING] We'll first open Firebase.js
and create a constant to store an instance of
messaging, which is created by calling getMessaging. The FCM web interface
uses web credentials called Voluntary Application
server identification or VAPID keys to otherwise send requests
to supported web push services. To subscribe the app
to push notifications, we'll need to associate a
pair of keys with our Firebase project. Let's see how we can
generate a new key pair in Firebase Console. We'll go to Project Settings,
and go to Cloud Messaging. Down here in the Web
Configuration section, we see a Web Push
Certificates tab. Now, let's click
Generate key pair. The Console displays a
notice that the key pair was generated and displays
the public key string and date added. What do we do with this? Well, let's copy it and
open the messaging.js file. Let's create a constant
VAPID_KEY paste it here. When notifications have
been enabled on a browser, we'll be given a device token. This device token
is what gets used to send a notification
to a particular browser. Let's write a function called
saveMessagingDeviceToken, in which we'll get a FCM
device token from the browser and save it to Cloud Firestore. We'll create a constant to
store the current token. And we'll use the
method getToken, which takes the messaging
instance as the first argument and takes the VAPID key as an
option in the second argument. The function returns a promise
containing the token string. So let's await it, and
if it returns a token, let's print it out to the
Console for debugging purposes for later. After, we'll save the device
token to Cloud Firestore. To do so, we'll
create a reference to where we want to store
this device token in Firestore by using the doc function. A Firestore instance
is the first argument, and the second
argument is the path to the document, which is the
collection name in this case. Let's create a constant to
store the name of the FCM token collection fcmTokens. We'll use that in
the doc function, and the third argument is the
identifier for the document. Since we want to associate
each FCM token with a user, we'll just use the user ID. Since we need the
user ID here, let's take that as an argument to
this saveMessagingDeviceToken function. All right, now, using setDoc,
we can add the FCM token value for this user. And as a reminder, setDoc
will overwrite the document if it already exists, meaning
if the user already has a FCM token, the value in Firestore
will get overwritten, which is what we want in this case. This is fine for one getToken
successfully retrieves a token, but it's not going
to work initially. For the app to be able to
retrieve the device token, the user needs to grant
the app permission to show notifications. So if getToken doesn't
retrieve a token, we'll need to
request permissions to show notifications. Let's write another function
requestNotificationsPermissions, and we'll create a permission
constant permission. We'll call the built-in
notification web API to request permission
and await the results. If permission is granted, we'll
call saveMessagingDeviceToken. And since this function
requires the UID, we'll take the
UID as a parameter to
requestNotificationsPermissions and pass it in when we call it
in saveMessagingDeviceToken as well. If it fails, we'll just log
for now that we cannot get permission. A better way to do this is
to propagate it up to the UI and let the user know
to check the page later, as opposed to being
automatically notified of when to confirm the expense. Now that we have these
functions written, where shall we call
saveMessagingDeviceToken? Well, we want to
request permissions to show notifications when
the user submits an expense so that the app can notify the user
that the receipt is ready to be confirmed on the app. We'll open expenseDialog.js, and
call saveMessagingDeviceToken after we store the
image into storage. In order to receive
notifications, the app must define the Firebase
Messaging Service worker in Firebase-messaging-sw.js. And this file goes
in the public folder. Let's open that. We'll need to
initialize the app here. So let's copy the Firebase
Config from Firebase.js, and then we'll
initialize the app and get an instance
of Firebase Messaging. Just as I note, this instance
of Firebase Messaging is an instance of one
from the service worker. So it's a bit different
than the instance we retrieved in Firebase.js. Great, let's try
it on our app now. Whoops, when we
initially open the app, there's an error saying that
service messaging is not available. To fix the problem, there is a
function in the FCM JavaScript SDK called isSupported, which
returns a promise resolving to true if a FCM instance
can be initialized in the current environment
and false if not. Let's use that and
await the results. If it's true, then we
can get the FCM instance by calling getMessaging. We'll make all of
this a function because otherwise, we're
using a wait on global scope, and there is no
guarantee on when the instance will be returned. As a function, we can
wait on the return value before doing anything else. This means that every time we
want to use a FCM instance, we'll need to call this
messaging function. Let's go back to
messaging.js where we need to use this messaging instance. And we'll change the code
to have a constant, msg to store the return value
of calling the messaging function we just wrote. Instead of the old messaging
constant, we'll use msg. So here in getToken. With this fixed, let's see
whether our app works now. It does. To remind ourselves of
what we were doing before, we have just written
saveMessagingDeviceToken and
requestNotificationsPermissions, and we want to see
whether that works. And we call
saveMessagingDeviceToken after a user uploads an
image to Cloud storage. Let's click on adding an
expense and upload an image. When we click on Submit, we
should be asked for permission to show notifications. And indeed, we are. Let's allow that. Let's take a look at
the JavaScript Console. We printed out the FCM
device token earlier. And so this confirms that we can
get permission and after we do, we're able to get the token. [MUSIC PLAYING] Now, how are we going to
actually see the notification? Well, first we need
to write some code to send the actual notification. Since we want the user
to receive a notification upon the Google Vision API
finishing its processing and adding the new
expense to the Firestore, we'll write the code to send
a notification in our Cloud Function, which has all the
expense processing code. Earlier in messaging.js, we
stored the users FCM token into the Firestore. We're going to need to
retrieve that token, so we know where to
send this notification. So we'll create a
documentSnapshot constant, and use the admin for
Firestore to get the FCM token collection. And we specifically want to
document what the user UID, which we already extracted,
we'll get the document. And in another
constant token, we'll get documentSnapshots.data,
and get the FCM token. Now, we know where to
send this notification, and we'll have to craft
the notification message. Let's make the
message, your receipt is ready for review
and confirmation. And the payload will include
the token, the notification with a title which we'll
set as expense tracker your expense has been processed
and the body with the message. We'll then use admins
messaging to send a payload and upon receiving
a response, we'll log it using
functions.logger.log to note that we did
indeed successfully send the message, just
for debugging purposes. If we get an error,
let's log that as well. With our updated function,
we'll have to deploy it. So let's open the
terminal and run firebase deploy
--only functions. While that happens,
let's figure out how to actually receive
the notification. So with FCM, the
behavior of messages differs depending on
whether the page is in the foreground,
meaning it has focus, or in the background
hidden behind other tabs or completely closed. In all cases, the page must
handle the onMessage callback. So let's implement that. [MUSIC PLAYING] With our service worker
set up from before, we can handle messages when
our app is in the foreground. We'll go back to messaging.js. And after we set the
document in Firestore, we'll call onMessage
to console.log that we're going to
send a notification from the foreground, which is
useful for debugging purposes. Then we're going to use
the notification web API to send a notification
with the messages notification title and body. Let's check whether this works. Our function is now deployed,
and so if we open the web app and add an expense,
choose an image to submit, and wait a bit while the
app is in the foreground, we shall see the notification. And here it is. Yay. Now, what happens if the
app is in the background? Well, we'll have to go back to.
firebase -messaging-sw.js just to take care of that. We'll use
messaging.onBackgroundMessage. And we'll console.log that
we've received a background message, again, just
for debugging purposes. We'll then extract
the notification title from the payload by doing
payload.notification.title. And we'll create
notification options with the body of
payload.notification.body. We'll use self.registratio
n.showNotification of the notification title
and notification options. Let's go back to the app. And this is a bit tricky. It took me a bit of
time to figure out. Because sometimes, when I edited
this firebase-messaging-sw.js file, the code didn't
actually update. To mitigate this issue, and
check that it does update, we can inspect the
page, go to application, and click on Service Workers. There, we can see the currently
running Service Workers, and if we click on it,
we can see the code that's currently running. And we see that this
background message function we just wrote is not there. To fix that, we can click
on this unregister button here on the right, which
unregisters the Service Worker. And if we refresh the page, we
don't see any Service Workers. If we try again to add an
expense, pick an image, and submit it, we can now
see the Service Worker here and the associated code. And we see the
onBackgroundMessage function now, great. TO make sure it's working,
let's bring another page to the foreground, so our
app is in the background. And let's wait a moment. Oh, here's the notification. And just to make sure, we
can check the console.log for background message to make
sure the notification is indeed being triggered from the
onBackgroundMessage function and not the foreground one. Just for fun, let's also make
sure we get a notification if the app is not open at all. We'll add an expense again,
choose an image to upload, and submit. Upon getting the confirmation
that the expense was uploaded, let's close the app. Wait a moment and voila,
a notification appears. Yay. So with all of this,
now we have successfully sent a notification to a user
when an image gets uploaded regardless of
whether the user has the app open in the
foreground, in the background, or not at all. [MUSIC PLAYING] Now, FCM is useful for when
we want to send notifications as a result of user actions
like we just implemented, but we might also want
to send notifications as reminders that are not
based on user actions, such as a weekly reminder
to upload expenses. Now, it might be
annoying to users who are dedicated to
uploading their expenses, to get a notification
reminding them to upload expenses
every single week, as they might be wondering
why this app is sending them reminders when
they're already doing the thing the notification
is reminding them to do. Conveniently, as if we already
foresaw this happening, FCM is integrated
with capabilities to choose a user segment to
receive our notification. In our case, we can
only send notifications to users who haven't
opened the app for a week. Let's see how we can set
up a FCM messaging campaign in Firebase Console to do this. You'll see messaging. Let's go ahead and click
on Create First Campaign. We're going to send
notification messages, so let's choose that
and click Create. We'll need to fill
out some fields here, so for the notification title,
let's put in Expense Tracker reminder to input expenses. In the notification
text, we can say, Have any expenses from the last
week that you didn't input yet? Go to the expense tracker
and get those entered, so you have a complete
view of your expenses. After we fill that
out, we see that we can send a test message. Let's try that. If we click on
the button, we see needing to add a FCM
registration token. Now, this is the FCM token that
we've retrieved in our code and added to Firestore
in messaging.js. And that's the same FCM
token we get from Firestore in our function's code. Conveniently, we printed
out this FCM token, when we were writing
our messaging.js code. So we can simply
go back to our app and grab that from the
JavaScript Console. Let's copy the printed
token and then paste it into Firebase Console. After clicking on
the plus button, we see that it's selected,
and now we can click Test. Once we do, we can see how
our notification looks, great. On Firebase Console,
we'll hit Next. And this is where we'll
pick the user segment that will receive this notification. We have to pick the app for
which the user is using, and there's only one option
here, our expense tracker web app. We can add another
condition by clicking And. And then we can select
Last app engagement. Since we want to send
this notification to users who have not opened our
app for more than a week, we'll pick more than,
and put in seven days. We then click on
Next, which will then bring us to scheduling. We'll probably want to
run this every week. So we'll pick recurring
notifications every one week. Which day do we want to run it? Well, up to you. Since the day I'm
recording this is Tuesday, I'll just leave it at that. What time? I'll just leave it
at the default time, and we can leave all
the other fields as is. As a note, we can
always come back and change the time and end date
even after the campaign starts running. We don't need to do
anything else here, so we'll click
Review then publish. In order to show
you this, I will need to have an account that I
haven't logged into for exactly seven days at this moment,
which unfortunately, I didn't time well enough to do. So in order to show you that
these notification campaigns do indeed work, I'm going to go
back and remove the condition for last app engagement. So this is how you
edit a campaign. There's these three dots here,
and we can click on Edit. We can leave the text
as is, and click Next. Then hover over this
last engagement row, and click on the X here
to remove the condition. Let's review and publish. If we wait a few
minutes, a notification should show up,
soon, now, please. There we go, phew. So that's what should
happen every week. And well, since I can't
exactly show you this, you'll just have to trust me. So there you have it. Using Firebase
Cloud Messaging, we were able to send
users a notification that their expense was
processed and ready to confirm. And we set up a weekly
notification campaign through a Firebase
Console to remind those who have not
opened the app for a week to submit some expenses. I hope this was
helpful, and as this is the final video
in the series, it's been a great
journey for me. And thanks for following
the journey if you've gone through the whole series. Please let me know if
you have any feedback. And in the meantime,
happy Firebasing. [MUSIC PLAYING]