Last active
April 6, 2024 20:27
-
-
Save toonverbeek/5851b631be9a5ad6fb491988db84cbfb to your computer and use it in GitHub Desktop.
Sending e-mails on a schedule with Firebase Functions and Sendgrid
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// index.js | |
// inititalize firebase | |
const functions = require('firebase-functions'); | |
const admin = require('firebase-admin'); | |
// install by running "yarn add date-fns" | |
const { isPast, addHours } = require('date-fns'); | |
admin.initializeApp(); | |
// db is a global constant so that it can be re-used efficiently for each firebase function | |
// see https://firebase.google.com/docs/functions/tips#use_global_variables_to_reuse_objects_in_future_invocations | |
const db = admin.firestore(); | |
exports.onUserUpdate = admin.firestore.document('users/{uid}').onUpdate(handleCreateScheduledEmails); | |
exports.sendScheduledEmails = functions.pubsub.schedule('every 60 minutes').onRun(handleSendScheduledEmails); | |
async function handleCreateScheduledEmails(snap, context) { | |
const { confirmed, emailAddress } = snap.data().after; | |
const wasPreviouslyConfirmed = snap.data().before.confirmed; | |
const isUserConfirming = confirmed && !wasPreviouslyConfirmed | |
// 1. Check if the user is confirming their e-mail address | |
if (!isUserConfirming) return null; | |
// 2. Create welcomeEmail | |
const welcomeEmail = createWelcomeEmail(emailAddress); | |
// 3. Create howTo email | |
const howToEmail = createHowToEmail(emailAddress); | |
// 4. Return a single promise that resolves if all provided promises also resolve | |
return Promise.all([welcomeEmail, howToEmail]); | |
} | |
async function handleSendScheduledEmails(snap, context) { | |
const scheduledEmailsResult = await getScheduledEmails(); | |
const promises = []; | |
scheduledEmailsResult.forEach((emailDoc) => { | |
const email = emailDoc.data(); | |
const { scheduledDate, sendgridEmail, status } = email; | |
// .toDate() is needed because this is a Firebase Timestamp | |
if (isPast(scheduledDate.toDate()) && status === 'PENDING') { | |
promises.push(sendEmail(sendgridEmail, emailDoc.ref)); | |
} | |
}); | |
// We're using .allSettled() because the results are not dependent on each other | |
// i.e: if one fails, it should continue executing other promises | |
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/allSettled | |
const promiseResults = await Promise.allSettled(promises); | |
for (const result of promiseResults) { | |
if (result.status === 'rejected') { | |
functions.logger.error(`Could not send scheduled email: `, result.reason); | |
} | |
} | |
return null; | |
} | |
function createWelcomeEmail(emailAddress) { | |
const today = admin.firestore.Timestamp.now(); | |
const scheduledDate = addHours(today.toDate(), 4); | |
const emailConfig = { | |
to: emailAddress, | |
type: 'welcomeEmail', | |
scheduledDate | |
} | |
return createScheduledEmail(emailConfig); | |
} | |
function createHowToEmail(emailAddress) { | |
const today = admin.firestore.Timestamp.now(); | |
const scheduledDate = addHours(today.toDate(), 24); | |
const data = { firstName: "Bob" } | |
const emailConfig = { | |
to: emailAddress, | |
type: 'howToEmail', | |
data, | |
scheduledDate | |
} | |
return createScheduledEmail(emailConfig); | |
} | |
async function createScheduledEmail({ to, type, data, scheduledDate }) { | |
const scheduledEmail = { | |
scheduledDate, | |
status: 'PENDING', // PENDING, COMPLETED, ERROR | |
sendgridEmail: { | |
to, | |
template: { name: type, data } | |
} | |
}; | |
return db.collection('scheduledEmails').add(scheduledEmail); | |
} | |
async function getScheduledEmails() { | |
return db.collection('scheduledEmails') | |
.where('scheduledDate', '<=', admin.firestore.Timestamp.now()) | |
.where('status', '==', 'PENDING') | |
.orderBy('scheduledDate') | |
.get(); | |
} | |
async function sendEmail(email, scheduledEmailRef) { | |
try { | |
await db.collection('emails').add(email); | |
return scheduledEmailRef.update({ status: 'COMPLETED' }); | |
} catch (error) { | |
await scheduledEmailRef.update({ status: 'ERROR', error: error.message }); | |
return Promise.reject(error.message); | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
I love that approach! Thanks for providing this example!