Skip to content

Instantly share code, notes, and snippets.

@toonverbeek
Last active April 6, 2024 20:27
Show Gist options
  • Save toonverbeek/5851b631be9a5ad6fb491988db84cbfb to your computer and use it in GitHub Desktop.
Save toonverbeek/5851b631be9a5ad6fb491988db84cbfb to your computer and use it in GitHub Desktop.
Sending e-mails on a schedule with Firebase Functions and Sendgrid
// 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);
}
}
@techEgab
Copy link

techEgab commented Apr 6, 2024

I love that approach! Thanks for providing this example!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment