In the previous five stories, I showed the step-by-step creation of a Next.js 14 app that can be used to create Azure Resources via App Service. In the last step, we added the payment option with Stripe. Now, we will refine it and load the products directly from Stripe so that we have a full dynamic solution.
Github Repo and Example Page with Final Result
More details and the GitHub repo with the entire code
We only have to add/adapt four files to achieve our goal.
New subscription page
We added a new intermediate page that shows the available products.
src/app/[locale]/subscriptions/page.tsx
"use client";
import { useState, useEffect } from "react";
import { useRouter } from "next/navigation";
import getStripe from "@/lib/get-stripe";
export default function Subscriptions() {
const [products, setProducts] = useState([]);
const [loading, setLoading] = useState(true);
const router = useRouter();
// Fetch products from Stripe
useEffect(() => {
const fetchProducts = async () => {
try {
const res = await fetch("/api/stripe/get-products");
const data = await res.json();
setProducts(data.products);
} catch (error) {
console.error("Error fetching products:", error);
} finally {
setLoading(false);
}
};
fetchProducts();
}, []);
const handlePayment = async (priceId: any) => {
const res = await fetch("/api/stripe/create-checkout-session", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ priceId }),
});
const { id } = await res.json();
const stripe = (await getStripe()) as any;
const { error } = await stripe.redirectToCheckout({ sessionId: id });
if (error) console.error("Stripe checkout failed:", error);
else router.push("/dashboard");
};
if (loading) return <p>Loading subscriptions...</p>;
return (
<div className="container mx-auto p-4">
<h1 className="text-3xl font-bold mb-6 text-center">
Choose Your Subscription
</h1>
<div className="grid grid-cols-1 md:grid-cols-3 gap-8">
{products.map((product: any) => (
<div
key={product.id}
className="bg-white dark:bg-gray-800 p-6 rounded-lg shadow-md"
>
{product.images.length > 0 && (
<img
src={product.images[0]}
alt={product.name}
className="w-full h-48 object-cover rounded-lg mb-4"
/>
)}
<h2 className="text-xl font-semibold mb-2 text-gray-900 dark:text-gray-100">
{product.name}
</h2>
<p className="text-gray-700 dark:text-gray-300 mb-4">
{product.description}
</p>
<p className="text-lg font-bold text-gray-900 dark:text-gray-200 mb-6">
{product.default_price.unit_amount / 100}{" "}
{product.default_price.currency.toUpperCase()} /{" "}
{product.default_price.recurring.interval}
</p>
<button
onClick={() => handlePayment(product.default_price.id)}
className="w-full bg-blue-600 text-white py-2 rounded-lg hover:bg-blue-700 transition"
>
Choose {product.name}
</button>
</div>
))}
</div>
</div>
);
}New route for getting the products from the Stripe API
As you probably have seen, we are using a new API route on the subscription page to fetch the needed products from Stripe.
src/app/api/stripe/get-products/route.ts
import { NextRequest, NextResponse } from "next/server";
import Stripe from "stripe";
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!, {
apiVersion: "2024-09-30.acacia",
});
export async function GET(req: NextRequest) {
try {
const products = await stripe.products.list({
expand: ["data.default_price"],
});
return NextResponse.json({ products: products.data });
} catch (error) {
console.error("Error fetching products:", error);
return NextResponse.json({ error: "Error fetching products" });
}
}Adapting Checkout Session Route
Since we would like to sell “subscriptions” we modify the API route
src/app/api/stripe/create-checkout-session/route.ts
as well.
// /app/api/stripe/create-checkout-session/route.ts
import { NextRequest, NextResponse } from "next/server";
import Stripe from "stripe";
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!, {
apiVersion: "2024-09-30.acacia",
});
export async function POST(req: NextRequest) {
const { priceId } = await req.json();
try {
const session = await stripe.checkout.sessions.create({
payment_method_types: ["card"],
line_items: [{ price: priceId, quantity: 1 }],
mode: "subscription",
success_url: `${req.headers.get(
"origin"
)}/dashboard?session_id={CHECKOUT_SESSION_ID}`,
cancel_url: `${req.headers.get("origin")}/dashboard`,
});
// Set the session_id cookie
const response = NextResponse.json({ id: session.id });
response.cookies.set("session_id", session.id, {
path: "/",
httpOnly: true,
secure: process.env.NODE_ENV === "production",
});
return response;
} catch (error: any) {
return NextResponse.json({ error: error.message }, { status: 500 });
}
}Adapting the main component
Last but not least, we have to adapt the main component (src/components/user/createstorageaccountform_appservice.component.tsx)
"use client";
import React, { useState, useEffect } from "react";
import { useSession } from "next-auth/react";
import LoadingButton from "@/components/tools/loadingbutton/loadingbutton.component";
import FileUploader from "@/components/azure/storageaccounts/fileuploader.component";
import getStripe from "@/lib/get-stripe";
import { useRouter, useSearchParams } from "next/navigation";
// Utility function to generate a random alphanumeric string
const generateRandomName = (length: number) => {
const characters = "abcdefghijklmnopqrstuvwxyz0123456789";
return Array.from({ length }, () =>
characters.charAt(Math.floor(Math.random() * characters.length))
).join("");
};
const CreateStorageAccountFormAppService: React.FC = () => {
const { data: session } = useSession();
const user_az_id = session?.user?.sub;
const user_email = session?.user?.email;
const token = session?.accessToken;
const router = useRouter();
const searchParams = useSearchParams();
const session_id = searchParams.get("session_id");
const [resourceGroupName, setResourceGroupName] = useState(
generateRandomName(24)
);
const [accountName, setAccountName] = useState(generateRandomName(24));
const [location, setLocation] = useState("westeurope");
const [tags, setTags] = useState({
env: "production",
user: user_az_id || "",
});
const [containerName, setContainerName] = useState("mycontainer");
const [loading, setLoading] = useState(false);
const [message, setMessage] = useState("");
const [storageAccountName, setStorageAccountName] = useState("");
const [accessKey, setAccessKey] = useState("");
const [containers, setContainers] = useState<string[]>([]);
const [hasStorageAccount, setHasStorageAccount] = useState(false);
const [paid, setPaid] = useState(false); // Tracks payment status
const [azureOps, setAzureOps] = useState(""); // Tracks long-running operations (e.g., "create", "delete")
const [showModal, setShowModal] = useState(false); // Tracks modal visibility
// Function to check if the user has already paid
const checkPaymentStatus = async () => {
const res = await fetch("/api/stripe/check-payment-status");
const data = await res.json();
console.log("Payment status:", data.paid);
setPaid(data.paid);
};
// Function to check if the user already has a storage account
const checkIfUserHasStorageAccount = async () => {
if (!user_az_id) return;
try {
const response = await fetch(
"/api/azure/storageaccount/check-storage-account",
{
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ userId: user_az_id }),
}
);
if (response.ok) {
const data = await response.json();
setHasStorageAccount(data.storageAccountName !== "");
setStorageAccountName(data.storageAccountName);
setAccessKey(data.accessKey);
setContainerName(data.containerName || "mycontainer");
setResourceGroupName(data.resourceGroupName);
}
} catch (error) {
console.error("Error checking storage account existence:", error);
}
};
// Function to fetch the operation status (for long-running processes like creation or deletion)
// Funktion zum Abrufen des Werts aus der Datenbank
const fetchData = async () => {
try {
const operation = azureOps;
const value = "status";
// console.log("Operation: ", operation);
const responseOps = await fetch("/api/azure/resources/manageoperations", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
resourceGroupName,
operation,
value,
}),
});
const data = await responseOps.json();
// console.log("FetchData: ", data);
// console.log("resourceGroupName: ", resourceGroupName);
if (azureOps === "create") {
// console.log("Data: ", data.data.creation);
if (data.data.creation === "Success") {
setMessage("Storage account created successfully!");
setHasStorageAccount(true); // Update the state to reflect the creation
setLoading(false);
}
}
if (azureOps === "delete") {
// console.log("Data: ", data.data.deletion);
if (data.data.deletion === "Success") {
setMessage("Storage account successfully deleted!");
setHasStorageAccount(false); // Update the state to reflect the creation
setLoading(false);
}
}
} catch (error) {
console.error("Fehler beim Abrufen der Daten:", error);
}
};
// Handle Stripe Payment
const handlePayment = async () => {
router.push("/subscriptions");
// const res = await fetch("/api/stripe/create-checkout-session", {
// method: "POST",
// });
// const { id } = await res.json();
// const stripe = (await getStripe()) as any;
// const { error } = await stripe.redirectToCheckout({ sessionId: id });
// if (error) console.error("Stripe checkout failed:", error);
// else router.push("/dashboard");
};
// Create Storage Account after payment
const handleCreateStorageAccount = async () => {
setLoading(true);
setMessage("");
setAzureOps("create");
setContainers([]);
try {
let randomName = resourceGroupName;
const newTags = { env: "production", user: user_az_id };
// Check if Resource Group Name is empty
if (resourceGroupName === "") {
// console.error("Resource Group Name is empty");
randomName = generateRandomName(24); // Generate a new name
setResourceGroupName(randomName); // Update the state, but it may not reflect immediately
}
// Creating Records in Operationstable
const operation = "create";
const value = "pending";
// Call the function to create a record in the operation table
const responseOps = await fetch("/api/azure/resources/manageoperations", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
resourceGroupName: randomName, // Use the `randomName` directly
tags: newTags,
operation,
value,
}),
});
const response = await fetch(
"/api/azure/resources/createstorageaccountappservice",
{
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${token}`,
},
body: JSON.stringify({
resourceGroupName: randomName,
accountName,
location,
tags: newTags,
containerName,
user_az_id,
user_email,
}),
}
);
if (response.ok) {
const successData = await response.text();
// setMessage(`Success: ${successData}`);
checkIfUserHasStorageAccount(); // Refresh account state
} else {
const errorData = await response.text();
setMessage(`Error: ${errorData}`);
}
} catch (error: any) {
setMessage(`Error: ${error.message}`);
} finally {
setLoading(false);
}
};
// Delete storage account
const handleDeleteStorageAccount = async () => {
setLoading(true);
setMessage("");
setAzureOps("delete");
try {
// Creating Records in Operationstable
const operation = "delete";
const value = "pending";
// Call the function to create a record in the operation table
const responseOps = await fetch("/api/azure/resources/manageoperations", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
resourceGroupName,
operation,
value,
}),
});
const response = await fetch(
"/api/azure/resources/deleteresourcegroupappservice",
{
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${token}`,
},
body: JSON.stringify({
tagKey: "user",
tagValue: user_az_id,
user_email,
resourceGroupName,
}),
}
);
if (response.ok) {
const successData = await response.text();
// setMessage(`Success: ${successData}`);
checkIfUserHasStorageAccount(); // Refresh account state
} else {
const errorData = await response.text();
setMessage(`Error: ${errorData}`);
}
} catch (error: any) {
setMessage(`Error: ${error.message}`);
} finally {
setLoading(false);
}
};
// Show confirmation modal before deleting
const handleShowModal = () => {
setShowModal(true);
};
// Confirm and proceed to delete storage account
const confirmDeleteStorageAccount = () => {
setShowModal(false);
handleDeleteStorageAccount();
};
// Close modal without deleting
const closeModal = () => {
setShowModal(false);
};
useEffect(() => {
checkIfUserHasStorageAccount();
checkPaymentStatus();
if (session_id) {
fetch(`/api/stripe/success?session_id=${session_id}`)
.then((res) => res.json())
.then((data) => {
console.log(data.message); // Success message
setMessage(data.message);
});
}
}, [session]);
// Überwacht Datenbankwertänderungen
useEffect(() => {
// Datenbankabfrage alle 5 Sekunden
if (loading === false) return; // Nur abfragen, wenn der Ladezustand true ist
const intervalId = setInterval(fetchData, 5000);
return () => clearInterval(intervalId); // Bereinigen, wenn die Komponente unmontiert wird
}, [loading === true]);
return (
<div className="max-w-lg p-6 mx-auto mt-6 bg-gray-400 rounded-lg shadow-md">
<h2 className="mb-4 text-2xl font-bold text-center">
{hasStorageAccount
? "You already have a storage account"
: "Create Storage Account"}
</h2>
{!hasStorageAccount && !paid && (
<button
onClick={handlePayment}
className="w-full bg-blue-600 text-white px-4 py-2 rounded-lg hover:bg-blue-700 transition mb-4"
>
Proceed to Payment
</button>
)}
{!hasStorageAccount && paid && (
<LoadingButton
loading={loading}
onClick={handleCreateStorageAccount}
startlabel="Create Storage Account"
loadinglabel="Creating ..."
/>
)}
{hasStorageAccount && (
<>
<LoadingButton
loading={loading}
onClick={handleShowModal}
startlabel="Deleting Storage Account"
loadinglabel="Deleting ..."
/>
<div className="flex justify-center items-center min-h-screen">
<FileUploader
storageAccountName={storageAccountName}
accessKey={accessKey}
containerName={containerName}
/>
</div>
</>
)}
{message && (
<p className="mt-4 text-sm font-medium text-center">{message}</p>
)}
{containers.length > 0 && (
<div className="mt-6">
<h3 className="mb-2 text-lg font-semibold">Containers:</h3>
<ul className="list-disc list-inside">
{containers.map((container) => (
<li className="text-gray-600" key={container}>
{container}
</li>
))}
</ul>
</div>
)}
{/* Confirmation Modal */}
{showModal && (
<div className="fixed inset-0 flex items-center justify-center bg-black bg-opacity-75 z-50">
<div className="bg-white dark:bg-gray-800 rounded-lg shadow-lg max-w-md w-full p-6 relative">
{/* Modal Header */}
<div className="flex items-center justify-between mb-4">
<h3 className="text-xl font-semibold text-gray-800 dark:text-gray-200">
Confirm Deletion
</h3>
<button
onClick={closeModal}
className="text-gray-400 hover:text-gray-600 dark:hover:text-gray-300"
>
<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="M6 18L18 6M6 6l12 12"
/>
</svg>
</button>
</div>
{/* Modal Body */}
<p className="text-gray-700 dark:text-gray-300 mb-6">
Are you sure you want to delete the storage account? This action
is irreversible.
</p>
{/* Modal Footer */}
<div className="flex justify-end space-x-3">
<button
onClick={confirmDeleteStorageAccount}
className="bg-red-600 text-white px-4 py-2 rounded-md hover:bg-red-700 focus:ring-4 focus:ring-red-200 dark:focus:ring-red-900"
>
Yes, Delete
</button>
<button
onClick={closeModal}
className="bg-gray-300 text-gray-700 px-4 py-2 rounded-md hover:bg-gray-400 focus:ring-4 focus:ring-gray-200 dark:bg-gray-700 dark:text-gray-200 dark:hover:bg-gray-600 dark:focus:ring-gray-800"
>
Cancel
</button>
</div>
</div>
</div>
)}
</div>
);
};
export default CreateStorageAccountFormAppService;We only change the “handlePayment” function so that we can push the user to the previously created “subscription” page.
// Handle Stripe Payment
const handlePayment = async () => {
router.push("/subscriptions");
// const res = await fetch("/api/stripe/create-checkout-session", {
// method: "POST",
// });
// const { id } = await res.json();
// const stripe = (await getStripe()) as any;
// const { error } = await stripe.redirectToCheckout({ sessionId: id });
// if (error) console.error("Stripe checkout failed:", error);
// else router.push("/dashboard");
};New subscription page with Stripe products
Below, you can see the products fetched from Stripe on the new subscription page, and with a click on the “CTA” in the card, you will be pushed to the checkout.

Cloudapp-dev, and before you leave us
Thank you for reading until the end. Before you go:





