Loading...
nextjs14-building-saas-with-stripe-payment
Author Cloudapp
E.G.

Next.js 14 — Building a SaaS Solution on Azure with Stripe Integration — Part 5

November 27, 2024
Table of Contents

Github Repo and Example Page with Final Result

More details and the GitHub repo with the entire code

Here is a complete list of all previous stories so you can follow it from scratch.

Next.js 14 - Building a Saas solution on Azure (long-running processes) - Part 1

Next.js 14 - Building a Saas solution in Azure (authentication) - Part 2

Automated creation of Azure resources via CLI in Next.js - Part 3

Next.js 14 — Building a SaaS Solution on Azure (Storage Accounts etc.) — Part 4

NPM Packages

I am using these new packages:

"@stripe/stripe-js": "^4.7.0",
"@types/stripe-v3": "^3.1.33",
"stripe": "^17.1.0",

Small change on Prisma Schema

Below is my Prisma schema -> /Prisma/schema_storageaccounts.prisma

I added the field “updatedAt” to the model “operations,” so I have a clear overview of the last changes.

// This is your Prisma schema file,
// learn more about it in the docs: https://pris.ly/d/prisma-schema

generator client {
  provider      = "prisma-client-js"
  binaryTargets = ["native", "debian-openssl-1.1.x"]
  output        = "./generated/client_storageaccount"
}

datasource db {
  provider = "postgresql"
  url      = env("DATABASE_URL_STORAGEACCOUNTS")
}

model ResourceGroup {
  id              Int              @id @default(autoincrement()) // Primary key
  userId          String           @default("0") // The user ID
  name            String           @unique // Name of the resource group, unique
  createdAt       DateTime         @default(now()) // Creation date
  storageAccounts StorageAccount[] // Relation to StorageAccount model
}

model StorageAccount {
  id                 Int           @id @default(autoincrement()) // Primary key
  userId             String // The user ID
  storageAccountName String // Name of the storage account
  resourceGroupId    Int // Foreign key to ResourceGroup
  accessKey          String? // Access key for the storage account
  createdAt          DateTime      @default(now()) // Creation date
  containers         Container[] // Relation to Container model
  resourceGroup      ResourceGroup @relation(fields: [resourceGroupId], references: [id], onDelete: Cascade) // Relation to ResourceGroup

  @@unique([userId, storageAccountName]) // Unique constraint to ensure no duplicate storage accounts for a user
}

model Container {
  id               Int            @id @default(autoincrement()) // Primary key
  containerName    String // Name of the container
  storageAccountId Int // Foreign key to StorageAccount
  createdAt        DateTime       @default(now()) // Creation date
  storageAccount   StorageAccount @relation(fields: [storageAccountId], references: [id], onDelete: Cascade) // Relation to StorageAccount

  @@unique([storageAccountId, containerName]) // Unique constraint to ensure no duplicate container names within a storage account
}

model Operations {
  id            Int      @id @default(autoincrement()) // Primary key
  resourcegroup String // Name of the RGGroup
  creation      String   @default("NoOps") // Creation of the resource group
  deletion      String   @default("NoOps") // Deletion of the resource group
  createdAt     DateTime @default(now()) // Creation date
  updatedAt     DateTime @updatedAt // Last update date
}

New Stripe API Routes

We need new API Routes to communicate with Stripe. The first one is /src/app/API/stripe/check-payment-status, which checks the payment's status.

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) {
  const sessionId = req.cookies.get("session_id")?.value; // Assuming session ID is stored in cookies

  if (!sessionId) {
    return NextResponse.json({ paid: false });
  }

  try {
    const session = await stripe.checkout.sessions.retrieve(sessionId);
    return NextResponse.json({ paid: session.payment_status === "paid" });
  } catch (error) {
    console.error("Error retrieving session from Stripe:", error);
    return NextResponse.json({ paid: false });
  }
}

The next one is the create-checkout-session route, which we need to create the session and where we define the “success_url” and “cancel_url” for the redirect after the payment. In this API Route, we set a Session Cookie as well, for which we will need to check the payment status after a reload of the page.

// /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) {
  try {
    const session = await stripe.checkout.sessions.create({
      payment_method_types: ["card"],
      line_items: [
        {
          price_data: {
            currency: "usd",
            product_data: {
              name: "Azure Storage Account Subscription",
            },
            unit_amount: 5000, // $50
          },
          quantity: 1,
        },
      ],
      mode: "payment",
      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 });
  }
}

Last but not least we create a new “success” API Route

// /app/api/stripe/success/route.ts
import { NextRequest, NextResponse } from "next/server";
import Stripe from "stripe";
import { getToken } from "next-auth/jwt";

const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!, {
  apiVersion: "2024-09-30.acacia",
});

export async function GET(req: NextRequest) {
  const sessionId = req.nextUrl.searchParams.get("session_id");

  if (!sessionId) {
    return NextResponse.json(
      { error: "Session ID is required" },
      { status: 400 }
    );
  }

  const session = await stripe.checkout.sessions.retrieve(sessionId);

  if (session.payment_status === "paid") {
    const authtoken = await getToken({ req });

    if (!authtoken || authtoken.role !== "Admin") {
      return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
    }

    // Logic to create a storage account after payment.
    // Access session data and create Azure storage account.
    return NextResponse.json({
      message: "Payment successful, storage account creation is now possible!",
    });
  } else {
    return NextResponse.json(
      { error: "Payment not verified" },
      { status: 400 }
    );
  }
}

ENV Variables

As you can see in the API routes, we need new Env variables. I highly recommend to define production and test variables, you can distinguish them looking at the first part of the key “sk_live” vs “sk_test”.

# Stripe Prod
STRIPE_SECRET_KEY=sk_live_xxxxxxx
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=pk_live_xxxxx

# Stripe Test
STRIPE_SECRET_KEY=sk_test_xxxx
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=pk_test_xxxxx

Helper Library

With the env variables in place, we can create a new helper library.

// /lib/get-stripe.ts
import { loadStripe, Stripe } from "@stripe/stripe-js";

let stripePromise: Promise<Stripe | null>;

const getStripe = () => {
  if (!stripePromise) {
    stripePromise = loadStripe(process.env.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY!);
  }
  return stripePromise;
};

export default getStripe;

Adapting Manageoperations API

As mentioned in the Prisma schema section, I added a new field in the “operations” model. Therefore, we also have to change the logic for the “manage operations” API route. (src/app/api/azure/resources/manageoperations)

import { NextRequest, NextResponse } from "next/server";
import { getToken } from "next-auth/jwt";
import prisma_storageaccount from "@/lib/prisma_storageaccounts";

export const dynamic = "force-dynamic";

export async function POST(req: NextRequest) {
  const authtoken = await getToken({ req });
  const isAdmin = authtoken?.role === "Admin";
  if (!authtoken || !isAdmin) {
    return NextResponse.json(
      { error: "Unauthorized Operations API" },
      { status: 401 }
    );
  }

  try {
    const { resourceGroupName, operation, value } = await req.json();

    if (operation == "") {
      return NextResponse.json(
        { error: "Invalid request. Operation is missing" },
        { status: 400 }
      );
    }

    const creationvalue = operation === "create" ? value : "NoOps"; // If the operation is create, set the creation value to the value, otherwise set it to null
    const deleteionvalue = operation === "delete" ? value : "NoOps"; // If the operation is delete, set the deletion value to the value, otherwise set it to null

    const operationGetResult = await prisma_storageaccount.operations.findFirst(
      {
        where: {
          resourcegroup: resourceGroupName,
        },
      }
    );

    // If value is status, return the operation result
    if (value === "status") {
      return NextResponse.json({ data: operationGetResult }, { status: 200 });
    }

    let operationResult: any = "";

    if (!operationGetResult) {
      operationResult = await prisma_storageaccount.operations.create({
        data: {
          resourcegroup: resourceGroupName,
          creation: creationvalue,
          deletion: deleteionvalue,
        },
      });
    } else {
      if (operation === "create") {
        operationResult = await prisma_storageaccount.operations.update({
          where: {
            id: operationGetResult.id,
          },
          data: {
            creation: creationvalue,
            updatedAt: new Date(),
          },
        });
      } else if (operation === "delete") {
        operationResult = await prisma_storageaccount.operations.update({
          where: {
            id: operationGetResult.id,
          },
          data: {
            deletion: deleteionvalue,
            updatedAt: new Date(),
          },
        });
      }
    }

    return NextResponse.json({ message: operationResult }, { status: 200 });
  } catch (err) {
    console.error("Error processing request:", err);
    return NextResponse.json({ error: "Invalid request" }, { status: 400 });
  }
}

Main Component CreateStorageAccountform_Appservice

The biggest changes happened in our main component -> src/components/user/createstorageaccountform_appservice.components.tsx

We added two new imports

import getStripe from "@/lib/get-stripe";
import { useRouter, useSearchParams } from "next/navigation";

New Const Definitions

const router = useRouter();
const searchParams = useSearchParams();
const session_id = searchParams.get("session_id");
const [paid, setPaid] = useState(false); // Tracks payment status
const [showModal, setShowModal] = useState(false); // Tracks modal visibility

New checkPaymentStatus function

 // 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);
  };

Changes on handleDeleteStorageAccount and handleCreateStorageAccount.

// 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);
  };
{/* 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>
      )}

Complete code of the main component

Below is the complete code. You can also get it from the Github repo.

"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 () => {
    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;

UI Changes

With this change, the logged-in user will see this UI.

Make Payment
Make Payment

After a click on “Proceed to Payment” the user is redirected to Stripe (you can use test credit card data -> 4242 4242 4242 4242 - 01/28–123)

stripe payment
stripe payment

After the successful payment, the user will be redirected to “/dashboard,” and now the button changed to “Create Storage Account.”

successful payment
successful payment

Cloudapp-dev, and before you leave us

Thank you for reading until the end. Before you go:

Please consider clapping and following the writer! 👏 on our Medium Account

Or follow us on twitter -> Cloudapp.dev

Related articles