This is a guide on how to build a newsletter application that allows users to subscribe to a mailing list using a Google or GitHub account. It uses Next.js, Firebase, and Novu. Part 1.
In this 2-part tutorial, I’ll guide you through building a newsletter application that allows users to subscribe to a mailing list using a Google or GitHub account.
The application stores the user’s details (email, first and last names) to Firebase, enabling the admin user to send notifications of beautifully designed email templates to every subscriber within the application using Novu.
You’ll learn how to:
implement Firebase Google, GitHub, and Email/Password authentication to your applications
Create an admin folder within the app directory. Inside the admin folder, add a page.tsx file to represent the Admin home page and a layout.tsx file to display the page title.
cd appmkdir admin && cd admintouch page.tsx layout.tsx
Copy the code snippet below into the admin/page.tsx file. It displays the links to the Admin Sign-up and Login pages.
import Link from "next/link";export default function Admin() { return ( <main className='flex min-h-screen flex-col items-center justify-center p-8'> <h2 className='text-3xl font-bold mb-6'>Admin Panel</h2> <Link href='/admin/register' className='py-3 px-6 flex items-center justify-center rounded-md border-gray-600 border-2 w-3/5 mb-4 text-lg hover:bg-gray-800 hover:text-gray-50' > Sign Up as an Admin </Link> <Link href='/admin/login' className='py-3 px-6 flex items-center justify-center rounded-md border-gray-600 border-2 w-3/5 mb-4 text-lg hover:bg-gray-800 hover:text-gray-50' > Log in as an Admin </Link> </main> );}
Update the page title by copying this code snippet into the admin/layout.tsx file.
import type { Metadata } from "next";import { Sora } from "next/font/google";const sora = Sora({ subsets: ["latin"] });export const metadata: Metadata = { title: "Admin | Newsletter Subscription", description: "Generated by create next app",};export default function RootLayout({ children,}: Readonly<{ children: React.ReactNode;}>) { return ( <html lang='en'> <body className={sora.className}>{children}</body> </html> );}
Next, you need to create the Admin Register, Login, and Dashboard pages. Therefore, create the folders containing a page.tsx file.
Within the register/page.tsx file, copy and pasted the code snippet provided below. The code block displays the sign-up form where users can enter their email and password to create an admin user account.
Finally, let’s create the admin dashboard page. It displays all the available subscribers and a form enabling admin users to create and view the existing newsletters.
Copy the code snippet below into the dashboard/page.tsx file.
From the code snippet above, we have two components: the Newsletters and the SubscribersList.
The Newsletters component renders the existing newsletters and a form that allows the admin to create new ones. The SubscribersList component displays all the existing subscribers within the application.
Therefore, create a components folder that contains the Newsletters and SubscribersList components within the app folder.
cd appmkdir components && cd componentstouch SubscribersList.tsx Newsletters.tsx
Copy the code snippet below into the SubscribersList.tsx. It accepts the existing subscribers’ data as a prop and renders it within a table.
In this section, you’ll learn how to implement multiple authentication methods using Firebase within your application.
Subscribers will have the option to subscribe or sign in using either a Google or GitHub account, while Admin users will be able to sign in using the Email and Password authentication method.
First, create a util.ts file within the app folder and copy the provided code snippet into the file.
import { signInWithPopup } from "firebase/auth";import { auth } from "../../firebase";//👇🏻 Split full name into first name and last nameexport const splitFullName = (fullName: string): [string, string] => { const [firstName, ...lastNamePart] = fullName.split(" "); return [firstName, lastNamePart.join(" ")];};//👇🏻 Handle Sign in with Google and Githubexport const handleSignIn = (provider: any, authProvider: any) => { signInWithPopup(auth, provider) .then((result) => { const credential = authProvider.credentialFromResult(result); const token = credential?.accessToken; if (token) { const user = result.user; const [first_name, last_name] = splitFullName(user.displayName!); console.log([first_name, last_name, user.email]); } }) .catch((error) => { const errorCode = error.code; const errorMessage = error.message; console.error({ errorCode, errorMessage }); alert(`An error occurred, ${errorMessage}`); });};
The code snippet enables users to sign into the application via Google or GitHub authentication. It accepts authentication providers as parameters and logs the user’s email, first name, and last name to the console after a successful authentication process.
Next, you can execute the handleSignIn function within the app/page.tsx file when a user clicks the Sign Up with Google or GitHub buttons.
Before the GitHub authentication method can work as expected, you need to create a GitHub OAuth App.
Once you’ve created the GitHub OAuth App, copy the client ID and client secret from the app settings. You’ll need these credentials to activate the GitHub authentication method within the application.
Go back to the Firebase Console. Enable the GitHub sign-in method and paste the client ID and secret into the input fields provided. Additionally, add the authorisation callback URL to your GitHub app.
Congratulations! You’ve successfully added GitHub and Google authentication methods to the application. Next, let’s add the Email/Password authentication for Admin users.
Before we proceed, ensure you have enabled the email/password sign-in method within the Firebase project. Then, execute the functions below when a user signs up and logs in as an admin user.
import { createUserWithEmailAndPassword, signInWithEmailAndPassword,} from "firebase/auth";import { auth } from "../../firebase";//👇🏻 Admin Firebase Sign Up Functionexport const adminSignUp = async (email: string, password: string) => { try { const userCredential = await createUserWithEmailAndPassword( auth, email, password ); const user = userCredential.user; if (user) { //👉🏻 sign up successful } } catch (e) { console.error(e); alert("Encountered an error, please try again"); }};//👇🏻 Admin Firebase Login Functionexport const adminLogin = async (email: string, password: string) => { try { const userCredential = await signInWithEmailAndPassword( auth, email, password ); const user = userCredential.user; if (user) { //👉🏻 log in successful } } catch (e) { console.error(e); alert("Encountered an error, please try again"); }};
The provided code snippets include functions for admin user sign-up (adminSignUp) and login (adminLogin) using Firebase authentication. You can trigger these functions when a user signs up or logs in as an admin user.
You can sign users (admin and subscribers) out of the application using the code snippet below.
Congratulations! You’ve completed the authentication process for the application. You can read through the concise documentation if you encounter any issues.
In this section, you’ll learn how to save and retrieve data from Firebase Firestore by saving newsletters, subscribers, and admin users to the database.
Before we proceed, you need to add the Firebase Firestore to your Firebase project.
Create the database in test mode, and pick your closest region.
After creating your database, select Rules from the top menu bar, edit the rules, and publish the changes. This enables you to make requests to the database for a longer period of time.
Congratulations!🎉 Your Firestore database is ready.
After a user subscribes to the newsletter, you need to save their first name, last name, email, and user ID to Firebase. To do this, execute the saveToFirebase function after a subscriber successfully signs in via the GitHub or Google sign-in method.
import { auth, db } from "../../firebase";import { addDoc, collection, getDocs } from "firebase/firestore";export type SubscribersData = { firstName: string; lastName: string; email: string; id: string;};//👇🏻 Save the subscriber data to Firebaseconst saveToFirebase = async (subscriberData: SubscribersData) => { try { //👇🏻 checks if the subscriber already exists const querySnapshot = await getDocs(collection(db, "subscribers")); querySnapshot.forEach((doc) => { const data = doc.data(); if (data.email === subscriberData.email) { window.location.href = "/subscribe"; return; } }); //👇🏻 saves the subscriber details const docRef = await addDoc(collection(db, "subscribers"), subscriberData); if (docRef.id) { window.location.href = "/subscribe"; } } catch (e) { console.error("Error adding document: ", e); }};
The saveToFirebase function validates if the user is not an existing subscriber to prevent duplicate entries before saving the subscriber’s data to the database.
To differentiate between existing subscribers and admin users within the application, you can save admin users to the database.
Execute the provided function below when a user signs up as an admin.
import { db } from "../../firebase";import { addDoc, collection } from "firebase/firestore";//👇🏻 Add Admin to Firebase Databaseconst saveAdmin = async (email: string, uid: string) => { try { const docRef = await addDoc(collection(db, "admins"), { email, uid }); if (docRef.id) { alert("Sign up successful!"); window.location.href = "/admin/dashboard"; } } catch (e) { console.error("Error adding document: ", e); }};
Next, update the admin log-in function to ensure that the user’s data exists within the admin collection before granting access to the application.
import { auth, db } from "../../firebase";import { addDoc, collection, getDocs, where, query } from "firebase/firestore";//👇🏻 Admin Firebase Login Functionexport const adminLogin = async (email: string, password: string) => { try { const userCredential = await signInWithEmailAndPassword( auth, email, password ); const user = userCredential.user; //👇🏻 Email/Password sign in successful if (user) { //👇🏻 check if the user exists within the database const q = query(collection(db, "admins"), where("uid", "==", user.uid)); const querySnapshot = await getDocs(q); const data = []; querySnapshot.forEach((doc) => { data.push(doc.data()); }); if (data.length) { window.location.href = "/admin/dashboard"; alert("Log in successful!"); } } } catch (e) { console.error(e); alert("Encountered an error, please try again"); }};
To ensure that only authenticated users can access the Admin dashboard, Firebase allows you to retrieve the current user’s data at any point within the application. This enables us to protect the Dashboard page from unauthorised access and listen to changes in the user’s authentication state.
import { onAuthStateChanged } from "firebase/auth";import { auth } from "../firebase";export default function Dashboard() { const router = useRouter(); useEffect(() => { onAuthStateChanged(auth, (user) => { if (!user) { //👉🏻 redirect user to log in form router.push("/"); } }); }, [router]);}
Congratulations on making it thus far! 🎉 In the upcoming sections, you’ll learn how to create and send beautifully designed newsletters to subscribers using Novu and React Email.