In den vorangegangenen fünf Beiträgen habe ich die schrittweise Erstellung einer Next.js 14-App gezeigt, die zur Erstellung von Azure-Ressourcen über App Service verwendet werden kann. Im letzten Schritt haben wir die Zahlungsoption mit Stripe hinzugefügt. Jetzt werden wir sie verfeinern und die Produkte direkt von Stripe laden, so dass wir eine vollständig dynamische Lösung haben.
Github Repo und Beispielseite mit Endergebnis
Weitere Details und das GitHub-Repo mit dem gesamten Code
Wir müssen nur vier Dateien hinzufügen/anpassen, um unser Ziel zu erreichen.
Neue Abo-Seite
Wir haben eine neue Zwischenseite hinzugefügt, die die verfügbaren Produkte anzeigt.
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>
);
}Neue Route zum Abrufen der Produkte von der Stripe-API
Wie Sie wahrscheinlich gesehen haben, verwenden wir eine neue API-Route auf der Abonnement-Seite, um die benötigten Produkte von Stripe abzurufen.
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" });
}
}Anpassung der Route der Checkout-Sitzung
Da wir „Abonnements“ verkaufen möchten, ändern wir die API-Route
src/app/api/stripe/create-checkout-session/route.ts
// /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 });
}
}Anpassen der Hauptkomponente
Zu guter Letzt müssen wir die Hauptkomponente anpassen (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;Wir ändern nur die Funktion „handlePayment“, damit wir den Benutzer auf die zuvor erstellte Seite „subscription“ weiterleiten können.
// 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");
};Neue Abo-Seite mit Stripe-Produkten
Unten sehen Sie die von Stripe abgerufenen Produkte auf der neuen Abonnement-Seite, und mit einem Klick auf den „CTA“ in der Karte werden Sie zur Kasse geleitet.

Cloudapp.dev – Und bevor Sie uns verlassen:
Danke, dass Sie bis zum Ende gelesen haben. Noch eine Bitte bevor Sie gehen:
Wenn Ihnen gefallen hat was Sie gelesen haben oder wenn es Ihnen sogar geholfen hat, dann würden wir uns über einen "Clap" 👏 oder einen neuen Follower auf unseren Medium Account sehr freuen.





