Skip to main content

Build your React SaaS starter with Altogic and Stripe Part-2

· 26 min read
Deniz Çolak

Welcome back to part two of our tutorial series on building a React SaaS starter with Altogic and Stripe. In our previous post, we discussed the basics of setting up the project and integrating Stripe for handling payments. Now, we will dive deeper into the development process and explore how to manage customer subscriptions and billing information. By the end of this tutorial, you will have a fully functional SaaS starter that can handle payments and subscriptions, all powered by React, Altogic and the Stripe integration. So let's get started!

Introduction

In this part of the tutorial, we will build the front end with React and develop the pricing, subscriptions, invoices, account, payment success, and cancel pages to ensure an easy, fast, and best way of payment processing.

Prerequisite

Before starting this tutorial, please ensure you have completed the Altogic + React Authentication starter course to understand authentication, routing, session, etc.

User interface development

We will use React with the Tailwindcss to create basic project visit Tailwindcss & React Installation Guide. We will also use the same project structure as in the Altogic + React Authentication starter course to ship a fast MVP.

Creating pricing page

Let's start with creating a Pricing page for the existing app to collect payment for the different subscription plans.

So, let’s create Pricing.js component inside the components folder to display subscription plans with the buttons to subscribe to one.

To handle the pricing route, let's open the App.js and paste below code block.

import { BrowserRouter, Routes, Route } from "react-router-dom";
import { Home } from "./components/Home";
import { Signup } from "./components/Signup";
import { Verification } from "./components/Verification";
import { Redirect } from "./components/Redirect";
import { Login } from "./components/Login";
import { PrivateRoute } from "./components/PrivateRoute";
import { AuthProvider } from "./contexts/Auth";
import { Profile } from "./components/Profile";
import { Pricing } from "./components/Pricing";
export default function App() {
return (
<div>
<BrowserRouter>
<AuthProvider>
<Routes>
<Route
path="/"
element={
<PrivateRoute>
<Home />
</PrivateRoute>
}
/>
<Route
path="/profile"
element={
<PrivateRoute>
<Profile />
</PrivateRoute>
}
/>
<Route
path="/pricing"
element={
<PrivateRoute>
<Pricing />
</PrivateRoute>
}
/>
<Route path="/signup" element={<Signup />} />
<Route path="/auth-redirect" element={<Redirect />} />
<Route path="/verification" element={<Verification />} />
<Route path="/login" element={<Login />} />
</Routes>
</AuthProvider>
</BrowserRouter>
</div>
);
}

So, now we are ready to build the Pricing.js component, so open the Pricing.js file and copy the code block below.

import { useEffect, useState } from "react";
import { altogic } from "../helpers/altogic";
export function Pricing() {
const [priceData, setPriceData] = useState([]);

const getPriceData = async () => {
let { data } = await altogic.endpoint.get("products");
setPriceData(data.data);
console.log(data);
};

const subscription = async (id) => {
let { data } = await altogic.endpoint.post("subscription", {
priceID: id,
});

window.location.href = data.url;
};

useEffect(() => {
getPriceData();

// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
return (
<div className="bg-white">
<div className="max-w-7xl mx-auto py-24 px-4 sm:px-6 lg:px-8">
<div className="sm:flex sm:flex-col sm:align-center">
<h1 className="text-5xl font-extrabold text-gray-900 sm:text-center">
Choose your plan
</h1>
<p className="mt-5 text-xl text-gray-500 sm:text-center">
We've got the right plan for you. Choose from our monthly or daily
plan and get started today.
</p>
</div>
<div className="mt-12 space-y-4 sm:mt-16 sm:space-y-0 sm:grid sm:grid-cols-2 sm:gap-6 lg:max-w-4xl lg:mx-auto xl:max-w-none xl:mx-0 xl:grid-cols-4">
{priceData.map((plan) => (
<div
key={plan.id}
className="border border-gray-200 rounded-lg shadow-sm divide-y divide-gray-200"
>
<div className="p-6">
<h2 className="text-lg leading-6 font-medium text-gray-900">
{plan.nickname}
</h2>
<p className="mt-4 text-sm text-gray-500">
Start small and grow your business with our daily plan.
</p>
<p className="mt-8">
<span className="text-4xl font-extrabold text-gray-900">
${plan.unit_amount / 100}
</span>{" "}
<span className="text-base font-medium text-gray-500">
/ {plan.recurring.interval}
</span>
</p>
<a
data-id={plan.id}
key={plan.id}
href={plan.href}
onClick={() => {
subscription(plan.id);
}}
className="flex justify-center mt-8 w-full bg-gray-800 border border-gray-800 rounded-md py-2 text-sm font-semibold text-white text-center hover:bg-gray-900 hover:cursor-pointer"
>
Buy {plan.nickname}
</a>
</div>
</div>
))}
</div>
</div>
{/* Add centered full width card to display test credit card with number, expireDate, cvv */}
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="sm:flex sm:flex-col sm:align-center">
<h1 className="text-5xl font-extrabold text-gray-900 sm:text-center">
Test Credit Card
</h1>
<p className="mt-5 text-xl text-gray-500 sm:text-center">
Use this test credit card to test your payment integration.
</p>
<div className="mt-8">
<div className="border border-gray-200 rounded-lg shadow-sm divide-y divide-gray-200">
<div className="p-6">
<span className="text-md font-base text-gray-700">
When testing interactively, use a card number, such as{" "}
<strong>4242 4242 4242 4242</strong>. Enter the card number in
the Dashboard or in any payment form.
<br />
<br />
{/* Create an horizontal list */}
<ul className="list-disc list-inside">
<li>Use a valid future date, such as 12/34.</li>
<li>
Use any three-digit CVC (four digits for American Express
cards).
</li>
<li>Use any value you like for other form fields.</li>
</ul>
<br />
You can see more information about testing in Stripe docs.
</span>
</div>
</div>
</div>
</div>
</div>
</div>
);
}

With the help of the useEffect hook, we are getting price data by calling getPriceData() function and storing the response in the priceData state. In the defined endpoint, the response returns all the product plans, so we are mapping the priceData and displaying the plans on the page.

Pricing page

Once the user clicks the Buy button, we are triggering the subscription function with the id of a plan and navigating the user to the specified payment page with the selected id.

Stripe payment page

By calling the /subscription endpoint with the given id, We have defined two different paths for payment operations.

  1. Successfully payment.

    When the user successfully completes the payment, Altogic redirects the user to the /success route.

  2. Cancel the payment screen.

    When the user cancels the payment page, Altogic redirects the user to the /cancel route.

Let’s create Success.js and Cancel.js files inside of the /components folder.

And to display the pages, let us define these routes in App.js. Open the App.js and copy the below code block.

/src/App.js
import { BrowserRouter, Routes, Route } from "react-router-dom";
import { Home } from "./components/Home";
import { Signup } from "./components/Signup";
import { Verification } from "./components/Verification";
import { Redirect } from "./components/Redirect";
import { Login } from "./components/Login";
import { PrivateRoute } from "./components/PrivateRoute";
import { AuthProvider } from "./contexts/Auth";
import { Profile } from "./components/Profile";
import { Pricing } from "./components/Pricing";
import { Success } from "./components/Success";
import { Cancel } from "./components/Cancel";
export default function App() {
return (
<div>
<BrowserRouter>
<AuthProvider>
<Routes>
<Route
path="/"
element={
<PrivateRoute>
<Home />
</PrivateRoute>
}
/>
<Route
path="/profile"
element={
<PrivateRoute>
<Profile />
</PrivateRoute>
}
/>
<Route
path="/pricing"
element={
<PrivateRoute>
<Pricing />
</PrivateRoute>
}
/>
<Route
path="/cancel"
element={
<PrivateRoute>
<Cancel />
</PrivateRoute>
}
/>
<Route
path="/success"
element={
<PrivateRoute>
<Success />
</PrivateRoute>
}
/>
<Route path="/signup" element={<Signup />} />
<Route path="/auth-redirect" element={<Redirect />} />
<Route path="/verification" element={<Verification />} />
<Route path="/login" element={<Login />} />
</Routes>
</AuthProvider>
</BrowserRouter>
</div>
);
}

Creating successfully payment page

Previously, we created the Success.js component, so let’s open it and copy the following code.

import React from "react";
import { useAuth } from "../contexts/Auth";
import { altogic } from "../helpers/altogic";
import { useNavigate } from "react-router-dom";
export function Success() {
const { session, setUser } = useAuth();
const [countdown, setCountdown] = React.useState(3);
const [errors, setError] = React.useState(null);
const navigate = useNavigate();

// get user info from DB and set user state
React.useEffect(() => {
const getUserInfo = async () => {
const { user } = await altogic.auth.getUserFromDB();
setUser(user);
console.log(user);
if (errors) return setError(errors);
};
getUserInfo();
}, [errors, setUser]);

// countdown from 3 to 0 and redirect to subscriptions page
React.useEffect(() => {
const interval = setInterval(() => {
setCountdown((countdown) => countdown - 1);
}, 1000);
if (countdown === 0) {
clearInterval(interval);
navigate("/subscriptions");
}
return () => clearInterval(interval);
}, [countdown, navigate]);

return session ? (
<div>
{/* Display the redirecting page at the center of page and countdown from 3 to 0 */}
<div className="flex justify-center items-center h-screen">
<div className="flex flex-col justify-center items-center">
<div className="mx-auto flex items-center justify-center h-12 w-12 rounded-full bg-green-100">
<svg
className="h-6 w-6 text-green-600"
stroke="currentColor"
fill="none"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
d="M5 13l4 4L19 7"
/>
</svg>
</div>
<div className="mt-3 text-center sm:mt-5">
<h3
className="text-lg leading-6 font-medium text-gray-900"
id="modal-headline"
>
Payment successfully completed...
</h3>
{/* Display redirecting message to the user */}
<div className="mt-2">
<p className="text-sm text-gray-500 w-96">
You will be redirected to the subscriptions page in{" "}
<span className="font-bold">{countdown}</span> seconds.
</p>
</div>
</div>
</div>
</div>
</div>
) : (
<div className="flex justify-center items-center h-screen">
<div className="flex flex-col justify-center items-center">
<div className="mx-auto flex items-center justify-center h-12 w-12 rounded-full bg-red-100">
<svg
className="h-6 w-6 text-red-600"
stroke="currentColor"
fill="none"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
d="M6 18L18 6M6 6l12 12"
/>
</svg>
</div>
<div className="mt-3 text-center sm:mt-5">
<h3
className="text-lg leading-6 font-medium text-gray-900"
id="modal-headline"
>
Error
</h3>
<div className="mt-2">
<p className="text-sm text-gray-500 w-96">
{errors?.items?.map((error) => (
// display error items received from the server
<span key={error.message} className="text-red-500">
{error?.message}
</span>
))}
</p>
</div>
</div>
</div>
</div>
);
}

After a successful checkout, we use the useEffect hook to send a request to the Altogic by calling the getUserInfo function to get instant user information from Altogic and to update the user information in the state.

Success payment page

Also, here we have a countdown from 3 to 0 to navigate users to /subscription route to display subscriptions.

Creating payment cancelled page

We have already created the Cancel.js component, so let’s open it and copy the following code block.

import React from "react";
import { useAuth } from "../contexts/Auth";
import { altogic } from "../helpers/altogic";
import { useNavigate } from "react-router-dom";
export function Cancel() {
const { session, setUser } = useAuth();
const [countdown, setCountdown] = React.useState(3);
const [errors, setError] = React.useState(null);
const navigate = useNavigate();

// get user info from DB and set user state
React.useEffect(() => {
const getUserInfo = async () => {
const { user } = await altogic.auth.getUserFromDB();
setUser(user);
console.log(user);
if (errors) return setError(errors);
};
getUserInfo();
}, [errors, setUser]);

// countdown from 3 to 0 and redirect to pricing page
React.useEffect(() => {
const interval = setInterval(() => {
setCountdown((countdown) => countdown - 1);
}, 1000);
if (countdown === 0) {
clearInterval(interval);
navigate("/pricing");
}
return () => clearInterval(interval);
}, [countdown, navigate]);

return session ? (
<div>
{/* Display the redirecting page at the center of page and countdown from 3 to 0 */}
<div className="flex justify-center items-center h-screen">
<div className="flex flex-col justify-center items-center">
<div className="mx-auto flex items-center justify-center h-12 w-12 rounded-full bg-red-100">
<svg
className="h-6 w-6 text-red-600"
stroke="currentColor"
fill="none"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
d="M6 18L18 6M6 6l12 12"
/>
</svg>
</div>
<div className="mt-3 text-center sm:mt-5">
<h3
className="text-lg leading-6 font-medium text-gray-900"
id="modal-headline"
>
You have cancelled the payment...
</h3>
{/* Display redirecting message to the user */}
<div className="mt-2">
<p className="text-sm text-gray-500 w-96">
You will be redirected to the pricing page in{" "}
<span className="font-bold">{countdown}</span> seconds.
</p>
</div>
</div>
</div>
</div>
</div>
) : (
<div className="flex justify-center items-center h-screen">
<div className="flex flex-col justify-center items-center">
<div className="mx-auto flex items-center justify-center h-12 w-12 rounded-full bg-red-100">
<svg
className="h-6 w-6 text-red-600"
stroke="currentColor"
fill="none"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
d="M6 18L18 6M6 6l12 12"
/>
</svg>
</div>
<div className="mt-3 text-center sm:mt-5">
<h3
className="text-lg leading-6 font-medium text-gray-900"
id="modal-headline"
>
Error
</h3>
<div className="mt-2">
<p className="text-sm text-gray-500 w-96">
{errors?.items?.map((error) => (
// display error items received from the server
<span key={error.message} className="text-red-500">
{error?.message}
</span>
))}
</p>
</div>
</div>
</div>
</div>
);
}

The payment canceled page is almost the same as the Success page. The only difference is that in the Cancel component, we are navigating the user to the pricing page again to complete the payment.

Cancel payment

Creating subscriptions page

In the previous successful payment page, we have defined that to navigate users to the subscription page to display active subscriptions. So, let’s create a Subscriptions.js file inside of the components/ folder, and to show the subscriptions, let’s define /subscriptions route in the App.js.

Open App.js and copy the following code.

import { BrowserRouter, Routes, Route } from "react-router-dom";
import { Home } from "./components/Home";
import { Signup } from "./components/Signup";
import { Verification } from "./components/Verification";
import { Redirect } from "./components/Redirect";
import { Login } from "./components/Login";
import { PrivateRoute } from "./components/PrivateRoute";
import { AuthProvider } from "./contexts/Auth";
import { Profile } from "./components/Profile";
import { Pricing } from "./components/Pricing";
import { Success } from "./components/Success";
import { Cancel } from "./components/Cancel";
import { Subscriptions } from "./components/Subscriptions";
export default function App() {
return (
<div>
<BrowserRouter>
<AuthProvider>
<Routes>
<Route
path="/"
element={
<PrivateRoute>
<Home />
</PrivateRoute>
}
/>
<Route
path="/profile"
element={
<PrivateRoute>
<Profile />
</PrivateRoute>
}
/>
<Route
path="/pricing"
element={
<PrivateRoute>
<Pricing />
</PrivateRoute>
}
/>
<Route
path="/cancel"
element={
<PrivateRoute>
<Cancel />
</PrivateRoute>
}
/>
<Route
path="/success"
element={
<PrivateRoute>
<Success />
</PrivateRoute>
}
/>
<Route
path="/subscriptions"
element={
<PrivateRoute>
<Subscriptions />
</PrivateRoute>
}
/>
<Route path="/signup" element={<Signup />} />
<Route path="/auth-redirect" element={<Redirect />} />
<Route path="/verification" element={<Verification />} />
<Route path="/login" element={<Login />} />
</Routes>
</AuthProvider>
</BrowserRouter>
</div>
);
}

Open Subscriptions.js file and copy the below code to display and manage a list of subscriptions from Altogic.

import React from "react";
import { altogic } from "../helpers/altogic";
import { useAuth } from "../contexts/Auth";

export default function Subscriptions() {
const [subscriptions, setSubscriptions] = React.useState([]);
const { setUser } = useAuth();

async function getSubscriptions() {
const response = await altogic.endpoint.get("subscription/list");
setUser(response);
// set subscriptions in state
setSubscriptions(response.data.data);
}

// call handleCancelSubscription function
async function handleCancelSubscription(subscriptionId) {
const response = await altogic.endpoint.delete("subscription");
setUser(response);
}

// call getSubscriptions function on component mount
React.useEffect(() => {
getSubscriptions();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);

return (
<div>
<div className="max-w-7xl mx-auto pt-24 px-4 sm:px-6 lg:px-8">
<div className="sm:flex sm:flex-col sm:align-center">
<h1 className="text-5xl font-extrabold text-gray-900 sm:text-center">
Subscriptions
</h1>
<p className="mt-5 text-2xl w-full text-center items-center text-gray-500 sm:text-center">
Display plan of your subscriptions or purchases. View and display
the status of your subscriptions and ancel your subscriptions if you
want.
</p>
</div>
</div>
<div className="flex flex-col">
<div className="mt-12 mx-auto">
<div className="-my-2 overflow-x-auto sm:-mx-6 lg:-mx-8">
<div className="py-2 align-middle inline-block min-w-96 sm:px-6 lg:px-8">
<div className="shadow overflow-hidden border-b border-gray-200 sm:rounded-lg">
<table className="min-w-full divide-y divide-gray-200">
<thead className="bg-gray-50">
<tr>
<th
scope="col"
className="flex items-center align-baseline px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"
>
Plan name
</th>
<th
scope="col"
className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"
>
Subscription Date
</th>
<th
scope="col"
className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"
>
End Date
</th>

<th
scope="col"
className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"
>
Total
</th>
<th
scope="col"
className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"
>
Status
</th>
<th
scope="col"
className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"
>
Operations
</th>
</tr>
</thead>
<tbody className="bg-white divide-y divide-gray-200">
{subscriptions.map((subscription) => (
<tr key={subscription.id}>
<td
key={subscription.plan.nickname}
className="px-6 py-4 whitespace-nowrap text-sm text-gray-500"
>
{subscription.plan.nickname}
</td>
<td className="px-6 py-4 whitespace-nowrap">
<div className="text-sm text-gray-900">
{/* Convert epoch to readable format */}
{new Date(
subscription.start_date * 1000
).toLocaleDateString()}
</div>
</td>

<td className="px-6 py-4 whitespace-nowrap">
<div className="text-sm text-gray-900">
{new Date(
subscription.current_period_end * 1000
).toLocaleDateString()}
</div>
</td>
<td className="px-6 py-4 whitespace-nowrap">
<div className="text-sm text-gray-900">
{subscription.plan.amount / 100}{" "}
{subscription.plan.currency.toUpperCase()}
</div>
</td>
<td className="flex items-center px-6 py-4 whitespace-nowrap text-sm text-gray-500">
{/* Capitalize first character */}
{/* If subscription.status == active display green check icon */}
{subscription.status === "active" ? (
<svg
xmlns="http://www.w3.org/2000/svg"
className="h-6 w-6 text-green-500"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M5 13l4 4L19 7"
/>
</svg>
) : (
""
)}
{subscription.status.charAt(0).toUpperCase() +
subscription.status.slice(1)}
</td>
<td className="px-6 py-4 whitespace-nowrap text-right text-sm font-medium">
<div className="flex flex-row items-center">
<span
className="h-6 w-6 text-red-500 cursor-pointer"
onClick={() => {
handleCancelSubscription({
subscriptionId: subscription.id,
});
}}
>
Unsubscribe
</span>
</div>
</td>
</tr>
))}
</tbody>
</table>
</div>
<div className="mt-10 flex flex-row mx-auto justify-center">
<a
href="/invoices"
className="bg-gray-600 border border-transparent rounded-md shadow-sm py-2 px-4 inline-flex justify-center text-sm font-medium text-white hover:bg-gray-800 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-gray-500"
>
View invoices
</a>
</div>
</div>
</div>
</div>
</div>
</div>
);
}

In the above Subscription component, with the help of the useEffect hook triggering getSubscriptions function to get the list of the subscriptions and to display it in the frontend.

Subscriptions page

We added an unsubscribe button to the list. Once the user clicks the button, we are calling handleCancelSubscription function with the subscriptionId to unsubscribe from the plan with the following code block;

// call handleCancelSubscription function
async function handleCancelSubscription(subscriptionId) {
const response = await altogic.endpoint.delete("subscription");
// you can console.log(response) and display user object on the console
// console.log(response)
}

Also, we can display invoices by clicking the View invoices button. As we define in the code, it navigates the user to the /invoices route.

Creating invoices page

So, let’s create an Invoices.js file inside the components folder and open App.js to implement the /invoices route.

import { BrowserRouter, Routes, Route } from "react-router-dom";
import { Home } from "./components/Home";
import { Signup } from "./components/Signup";
import { Verification } from "./components/Verification";
import { Redirect } from "./components/Redirect";
import { Login } from "./components/Login";
import { PrivateRoute } from "./components/PrivateRoute";
import { AuthProvider } from "./contexts/Auth";
import { Profile } from "./components/Profile";
import { Pricing } from "./components/Pricing";
import { Success } from "./components/Success";
import { Cancel } from "./components/Cancel";
import { Subscriptions } from "./components/Subscriptions";
import { Invoices } from "./components/Invoices";
export default function App() {
return (
<div>
<BrowserRouter>
<AuthProvider>
<Routes>
<Route
path="/"
element={
<PrivateRoute>
<Home />
</PrivateRoute>
}
/>
<Route
path="/profile"
element={
<PrivateRoute>
<Profile />
</PrivateRoute>
}
/>
<Route
path="/pricing"
element={
<PrivateRoute>
<Pricing />
</PrivateRoute>
}
/>
<Route
path="/cancel"
element={
<PrivateRoute>
<Cancel />
</PrivateRoute>
}
/>
<Route
path="/success"
element={
<PrivateRoute>
<Success />
</PrivateRoute>
}
/>
<Route
path="/subscriptions"
element={
<PrivateRoute>
<Subscriptions />
</PrivateRoute>
}
/>
<Route
path="/invoices"
element={
<PrivateRoute>
<Invoices />
</PrivateRoute>
}
/>
<Route path="/signup" element={<Signup />} />
<Route path="/auth-redirect" element={<Redirect />} />
<Route path="/verification" element={<Verification />} />
<Route path="/login" element={<Login />} />
</Routes>
</AuthProvider>
</BrowserRouter>
</div>
);
}

Open the Invoices.js file and copy the below code to display the user's invoices.

import React from "react";
import { altogic } from "../helpers/altogic";
import { useAuth } from "../contexts/Auth";

export function Invoices() {
const [invoices, setInvoices] = React.useState([]);
const { setUser } = useAuth();

async function getInvoices() {
const response = await altogic.endpoint.get("invoices");
setInvoices(response.data.data);
}

async function getSubscriptions() {
const response = await altogic.endpoint.get("subscription/list");
setUser(response);
}

// call getInvoices function on component mount
React.useEffect(() => {
getInvoices();
getSubscriptions();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);

return (
<div>
<div className="max-w-7xl mx-auto pt-24 px-4 sm:px-6 lg:px-8">
<div className="sm:flex sm:flex-col sm:align-center">
<h1 className="text-5xl font-extrabold text-gray-900 sm:text-center">
Invoices
</h1>
<p className="mt-5 text-xl text-gray-500 sm:text-center">
Display invoices of your subscriptions or purchases. . View and
download invoices in PDF format.
</p>
</div>
</div>
{/* If loading state true display loader */}

<div className="flex flex-col">
<div className="mt-12 mx-auto">
<div className="-my-2 overflow-x-auto sm:-mx-6 lg:-mx-8">
<div className="py-2 align-middle inline-block min-w-96 sm:px-6 lg:px-8">
<div className="shadow overflow-hidden border-b border-gray-200 sm:rounded-lg">
<table className="min-w-full divide-y divide-gray-200">
<thead className="bg-gray-50">
<tr>
<th
scope="col"
className="flex items-center align-baseline px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"
>
<svg
xmlns="http://www.w3.org/2000/svg"
className="h-6 w-6"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M7 20l4-16m2 16l4-16M6 9h14M4 15h14"
/>
</svg>
Invoice Number
</th>
<th
scope="col"
className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"
>
Invoice Date
</th>
<th
scope="col"
className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"
>
Due Date
</th>

<th
scope="col"
className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"
>
Total
</th>
<th
scope="col"
className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"
>
Status
</th>
<th
scope="col"
className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"
>
Operations
</th>
</tr>
</thead>
<tbody className="bg-white divide-y divide-gray-200">
{invoices.map((invoice) => (
<tr key={invoice.id}>
{/* Map invoice.line.data and display amount, type */}
<td className="px-6 py-4 whitespace-nowrap">
<div className="text-sm text-gray-900">
{invoice.number}
</div>
</td>
<td className="px-6 py-4 whitespace-nowrap">
<div className="text-sm text-gray-900">
{/* Convert epoch to readable format */}
{new Date(
invoice.created * 1000
).toLocaleDateString()}
</div>
</td>

{/* map invoice.lines.data to display, line.due_date */}

{invoice.lines.data.map((line) => (
<td
key={line.id}
className="px-6 py-4 whitespace-nowrap"
>
<div className="text-sm text-gray-900">
{new Date(
line.period.end * 1000
).toLocaleDateString()}
</div>
</td>
))}

{invoice.lines.data.map((line) => (
<td
key={line.id}
className="px-6 py-4 whitespace-nowrap text-sm text-gray-500"
>
{line.amount / 100} {line.currency.toUpperCase()}
</td>
))}
<td className="flex items-center px-6 py-4 whitespace-nowrap text-sm text-gray-500">
{/* Capitalize first character */}
{/* If invoice.status == paid display green check icon */}
{invoice.status === "paid" ? (
<svg
xmlns="http://www.w3.org/2000/svg"
className="h-6 w-6 text-green-500"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M5 13l4 4L19 7"
/>
</svg>
) : (
""
)}
{invoice.status.charAt(0).toUpperCase() +
invoice.status.slice(1)}
</td>
<td className="px-6 py-4 whitespace-nowrap text-right text-sm font-medium">
{/* Display view, download, access svg icons in a row */}

<div className="flex flex-row items-center">
<a
href={invoice.hosted_invoice_url}
className="text-gray-700 hover:text-gray-900 mr-4"
>
View
</a>
<a
href={invoice.invoice_pdf}
className="text-gray-700 hover:text-gray-900"
>
Download
</a>

{/* Display this icon only for the first invoice */}
</div>
</td>
{/* Add delete icon with red color x button and onClick trigger handleCancelSubscription */}
</tr>
))}
</tbody>
</table>
</div>
<div className="mt-10 flex flex-row mx-auto justify-center">
<a
href="/subscriptions"
className="bg-gray-600 border border-transparent rounded-md shadow-sm py-2 px-4 inline-flex justify-center text-sm font-medium text-white hover:bg-gray-800 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-gray-500"
>
View subscriptions
</a>
</div>
</div>
</div>
</div>
</div>
</div>
);
}

In the above Invoices component, with the help of the useEffect hook, we are calling getSubscriptions and getInvoices functions to get the list of invoices. In the response JSON, the hosted_invoice_url and invoice_pdf fields are used to download and display invoices.

Invoices page

Now, Let's display the user information and plan on the account page.

Creating account page

Let’s create the Account.js file inside the components folder and open App.js to define the /account route.

import { BrowserRouter, Routes, Route } from "react-router-dom";
import { Home } from "./components/Home";
import { Signup } from "./components/Signup";
import { Verification } from "./components/Verification";
import { Redirect } from "./components/Redirect";
import { Login } from "./components/Login";
import { PrivateRoute } from "./components/PrivateRoute";
import { AuthProvider } from "./contexts/Auth";
import { Profile } from "./components/Profile";
import { Pricing } from "./components/Pricing";
import { Success } from "./components/Success";
import { Cancel } from "./components/Cancel";
import { Subscriptions } from "./components/Subscriptions";
import { Invoices } from "./components/Invoices";
import { Account } from "./components/Account";

export default function App() {
return (
<div>
<BrowserRouter>
<AuthProvider>
<Routes>
<Route
path="/"
element={
<PrivateRoute>
<Home />
</PrivateRoute>
}
/>
<Route
path="/profile"
element={
<PrivateRoute>
<Profile />
</PrivateRoute>
}
/>
<Route
path="/pricing"
element={
<PrivateRoute>
<Pricing />
</PrivateRoute>
}
/>
<Route
path="/cancel"
element={
<PrivateRoute>
<Cancel />
</PrivateRoute>
}
/>
<Route
path="/success"
element={
<PrivateRoute>
<Success />
</PrivateRoute>
}
/>
<Route
path="/subscriptions"
element={
<PrivateRoute>
<Subscriptions />
</PrivateRoute>
}
/>
<Route
path="/invoices"
element={
<PrivateRoute>
<Invoices />
</PrivateRoute>
}
/>
<Route
path="/account"
element={
<PrivateRoute>
<Account />
</PrivateRoute>
}
/>
<Route path="/signup" element={<Signup />} />
<Route path="/auth-redirect" element={<Redirect />} />
<Route path="/verification" element={<Verification />} />
<Route path="/login" element={<Login />} />
</Routes>
</AuthProvider>
</BrowserRouter>
</div>
);
}

And open the Account.js file and copy below code block.

import React from "react";
import { useAuth } from "../contexts/Auth";

export function Account() {
const { user } = useAuth();

return (
<div>
{/* Display profile card at the center of the page with gradient background*/}

<div className="bg-white">
<div className="max-w-7xl mx-auto py-12 px-4 sm:px-6 lg:px-8">
<div className="lg:text-center">
<p className="mt-2 text-3xl leading-8 font-extrabold tracking-tight text-gray-900 sm:text-4xl">
Account
</p>
<p className="mt-4 max-w-2xl text-xl text-gray-500 lg:mx-auto">
Display your profile and account information. You can also update
your password and email address.
</p>
</div>
{/* Create a container to display at the center */}

<div className="mt-10">
<dl className="space-y-10 sm:space-y-0 sm:grid sm:grid-cols-3 sm:gap-x-6 sm:gap-y-10 lg:grid-col-3 justify-items-start w-full">
<div className=" relative">
<dt>
<div className="absolute flex items-center justify-center h-12 w-12 rounded-md bg-gray-800 text-white">
{/* Heroicon name: profile */}
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
class="w-6 h-6"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M15.75 6a3.75 3.75 0 11-7.5 0 3.75 3.75 0 017.5 0zM4.501 20.118a7.5 7.5 0 0114.998 0A17.933 17.933 0 0112 21.75c-2.676 0-5.216-.584-7.499-1.632z"
/>
</svg>
</div>
<p className="ml-16 text-lg leading-6 font-medium text-gray-900">
Full name
</p>
</dt>
<dd className="ml-16 text-base text-gray-500">{user?.name}</dd>
</div>

<div className="relative">
<dt>
<div className="absolute flex items-center justify-center h-12 w-12 rounded-md bg-gray-800 text-white">
{/* Heroicon name: lightning-bolt */}
<svg
className="h-6 w-6"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
aria-hidden="true"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M13 10V3L4 14h7v7l9-11h-7z"
/>
</svg>
</div>
<p className="ml-16 text-lg leading-6 font-medium text-gray-900">
Plan
</p>
</dt>
{/* If user.plan === 'Free plan' display upgrade plan button near the user.plan info */}
<dd className="ml-16 text-base text-gray-500">
<span className="px-2 inline-flex text-xs leading-5 font-semibold rounded-md bg-gray-100 border-gray-900 shadow-sm">
<p className="text-sm font-normal text-gray-800">
{user?.plan}
</p>
</span>
{user?.plan === "Free Plan" ? (
<button
className="bg-gray-700 hover:bg-gray-900 text-white text-xs font-normal px-2 py-1 rounded ml-2"
onClick={() => {
window.location.href = "/pricing";
}}
>
Upgrade Plan
</button>
) : null}
</dd>{" "}
</div>
<div className="relative">
<dt>
<div className="absolute flex items-center justify-center h-12 w-12 rounded-md bg-gray-800 text-white">
{/* Heroicon name: email */}
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
class="w-6 h-6"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M9 12.75L11.25 15 15 9.75M21 12c0 1.268-.63 2.39-1.593 3.068a3.745 3.745 0 01-1.043 3.296 3.745 3.745 0 01-3.296 1.043A3.745 3.745 0 0112 21c-1.268 0-2.39-.63-3.068-1.593a3.746 3.746 0 01-3.296-1.043 3.745 3.745 0 01-1.043-3.296A3.745 3.745 0 013 12c0-1.268.63-2.39 1.593-3.068a3.745 3.745 0 011.043-3.296 3.746 3.746 0 013.296-1.043A3.746 3.746 0 0112 3c1.268 0 2.39.63 3.068 1.593a3.746 3.746 0 013.296 1.043 3.746 3.746 0 011.043 3.296A3.745 3.745 0 0121 12z"
/>
</svg>
</div>
<p className="ml-16 text-lg leading-6 font-medium text-gray-900">
Email verification
</p>
</dt>
<dd className="ml-16 text-base text-gray-500">
{/* If user.emailVerified === true display verified tag */}
{user?.emailVerified ? (
<span className="px-2 inline-flex text-xs leading-5 font-semibold rounded-md bg-green-100 text-green-800">
<p className="text-sm font-normal text-green-600">
Verified
</p>
</span>
) : (
<span className="px-2 inline-flex text-xs leading-5 font-semibold rounded-md bg-red-100 text-red-800">
<p className="text-sm font-normal text-red-600">
Not verified
</p>
</span>
)}
</dd>
</div>
<div className="relative">
<dt>
<div className="absolute flex items-center justify-center h-12 w-12 rounded-md bg-gray-800 text-white">
{/* Heroicon name: email */}
<svg
className="h-6 w-6"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
aria-hidden="true"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M12 19l9 2-9-18-9 18 9-2zm0 0v-8"
/>
</svg>
</div>
<p className="ml-16 text-lg leading-6 font-medium text-gray-900">
Email address
</p>
</dt>
<dd className="ml-16 text-base text-gray-500">{user?.email}</dd>
</div>

<div className="relative">
<dt>
<div className="absolute flex items-center justify-center h-12 w-12 rounded-md bg-gray-800 text-white">
{/* Heroicon name: email */}
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
class="w-6 h-6"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M7.864 4.243A7.5 7.5 0 0119.5 10.5c0 2.92-.556 5.709-1.568 8.268M5.742 6.364A7.465 7.465 0 004.5 10.5a7.464 7.464 0 01-1.15 3.993m1.989 3.559A11.209 11.209 0 008.25 10.5a3.75 3.75 0 117.5 0c0 .527-.021 1.049-.064 1.565M12 10.5a14.94 14.94 0 01-3.6 9.75m6.633-4.596a18.666 18.666 0 01-2.485 5.33"
/>
</svg>
</div>
<p className="ml-16 text-lg leading-6 font-medium text-gray-900">
Customer ID
</p>
</dt>
<dd className="ml-16 text-base text-gray-500">
{user?.customerID}
</dd>
</div>
<div className="relative">
<dt>
<div className="absolute flex items-center justify-center h-12 w-12 rounded-md bg-gray-800 text-white">
{/* Heroicon name: email */}
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
class="w-6 h-6"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M9 12.75L11.25 15 15 9.75M21 12c0 1.268-.63 2.39-1.593 3.068a3.745 3.745 0 01-1.043 3.296 3.745 3.745 0 01-3.296 1.043A3.745 3.745 0 0112 21c-1.268 0-2.39-.63-3.068-1.593a3.746 3.746 0 01-3.296-1.043 3.745 3.745 0 01-1.043-3.296A3.745 3.745 0 013 12c0-1.268.63-2.39 1.593-3.068a3.745 3.745 0 011.043-3.296 3.746 3.746 0 013.296-1.043A3.746 3.746 0 0112 3c1.268 0 2.39.63 3.068 1.593a3.746 3.746 0 013.296 1.043 3.746 3.746 0 011.043 3.296A3.745 3.745 0 0121 12z"
/>
</svg>
</div>
<p className="ml-16 text-lg leading-6 font-medium text-gray-900">
Phone verification
</p>
</dt>
<dd className="ml-16 text-base text-gray-500">
{/* If user.phoneVerified === true display verified tag */}
{user?.phoneVerified && user?.phoneVerified ? (
<span className="px-2 inline-flex text-xs leading-5 font-semibold rounded-md bg-green-100 text-green-800">
<p className="text-sm font-normal text-green-600">
Verified
</p>
</span>
) : (
<span className="px-2 inline-flex text-xs leading-5 font-semibold rounded-md bg-red-100 text-red-800">
<p className="text-sm font-normal text-red-600">
Not verified
</p>
</span>
)}
</dd>
</div>

<div className="relative">
<dt>
<div className="absolute flex items-center justify-center h-12 w-12 rounded-md bg-gray-800 text-white">
{/* Heroicon name: clock */}
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
class="w-6 h-6"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M12 6v6h4.5m4.5 0a9 9 0 11-18 0 9 9 0 0118 0z"
/>
</svg>
</div>
<p className="ml-16 text-lg leading-6 font-medium text-gray-900">
Last Login
</p>
</dt>
<dd className="ml-16 text-base text-gray-500">
{new Date(user?.lastLoginAt).toLocaleString()}
</dd>
</div>
<div className="relative">
<dt>
<div className="absolute flex items-center justify-center h-12 w-12 rounded-md bg-gray-800 text-white">
{/* Heroicon name: email */}
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
class="w-6 h-6"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M15 9h3.75M15 12h3.75M15 15h3.75M4.5 19.5h15a2.25 2.25 0 002.25-2.25V6.75A2.25 2.25 0 0019.5 4.5h-15a2.25 2.25 0 00-2.25 2.25v10.5A2.25 2.25 0 004.5 19.5zm6-10.125a1.875 1.875 0 11-3.75 0 1.875 1.875 0 013.75 0zm1.294 6.336a6.721 6.721 0 01-3.17.789 6.721 6.721 0 01-3.168-.789 3.376 3.376 0 016.338 0z"
/>
</svg>
</div>
<p className="ml-16 text-lg leading-6 font-medium text-gray-900">
Subscription ID
</p>
</dt>
<dd className="ml-16 text-base text-gray-500">
{user?.subscriptionID}
</dd>
</div>
<div className="relative">
<dt>
<div className="absolute flex items-center justify-center h-12 w-12 rounded-md bg-gray-800 text-white">
{/* Heroicon name: clock */}
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
class="w-6 h-6"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M12 6v6h4.5m4.5 0a9 9 0 11-18 0 9 9 0 0118 0z"
/>
</svg>
</div>
<p className="ml-16 text-lg leading-6 font-medium text-gray-900">
First Login
</p>
</dt>
<dd className="ml-16 text-base text-gray-500">
{new Date(user?.signUpAt).toLocaleString()}
</dd>
</div>
</dl>
{/* Display a button to navigate invoices page */}
<div className="mt-10 flex flex-row mx-auto justify-center">
<a
href="/invoices"
className="bg-gray-600 border border-transparent rounded-md shadow-sm py-2 px-4 inline-flex justify-center text-sm font-medium text-white hover:bg-gray-800 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-gray-500"
>
View invoices
</a>
</div>
</div>
</div>
</div>
</div>
);
}
Account page

Here we get the user object from the context and display it inside the HTML elements. Also, we add a conditional check to the plan field of the user object and display the upgrade plan button if the plan is 'Free plan'.

{
user?.plan === "Free Plan" ? (
<button
className="bg-gray-700 hover:bg-gray-900 text-white text-xs font-normal px-2 py-1 rounded ml-2"
onClick={() => {
window.location.href = "/pricing";
}}
>
Upgrade Plan
</button>
) : null;
}

In the end, we have already set up the pricing page, so for now, we do not want to extend the scope. This project will be a baseline for the Saas projects. We will develop the following features in the following tutorials.

  • Display the selected plan on the pricing page.
  • Create a dashboard page that can only be accessible to paid users.

All contributions are welcome for the above list!

You can access the source code of the tutorial from Github.

You can access the live demo from Vercel.

If you need help, join our Discord server and contact us.

Conclusion

We have completed the essential part of the payment processing. Now we can collect payments from the users, manage their new roles, display their active subscriptions, invoices, and more.