Skip to content

Commit

Permalink
feat: Add Stripe Payment
Browse files Browse the repository at this point in the history
  • Loading branch information
Prem Prakash Sharma committed Jul 29, 2024
1 parent 9ac115e commit 1af0758
Show file tree
Hide file tree
Showing 13 changed files with 540 additions and 62 deletions.
17 changes: 17 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@


# Stripe
STRIPE_SECRET_KEY={{STRIPE_SECRET_KEY}}
STRIPE_PUBLIC_KEY={{STRIPE_PUBLIC_KEY}}

# Razorpay
RAZORPAY_KEY_ID={{RAZORPAY_KEY_ID}}
RAZORPAY_KEY_SECRET={{RAZORPAY_KEY_SECRET}}

# Paypal
PAYPAL_CLIENT_ID={{PAYPAL_CLIENT_ID}}
PAYPAL_CLIENT_SECRET={{PAYPAL_CLIENT_SECRET}}

# Coinbase
COINBASE_API_KEY={{COINBASE_API_KEY}}
COINBASE_API_SECRET={{COINBASE_API_SECRET}}
57 changes: 57 additions & 0 deletions actions/stripe.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
"use server";
import Stripe from "stripe";
import products from "@/data/products.json";
import { NextResponse } from "next/server";

interface CreateCheckoutSessionInput {
productId: number;
quantity: number;
}

export async function createCheckoutSession({ productId, quantity }: CreateCheckoutSessionInput) {
if (!productId || !quantity) {
return { error: "Invalid input" };
}

const product = products.find((product) => product.id === productId);

if (!product) {
return { error: "Product not found" };
}

try {
const apiKey = process.env.STRIPE_SECRET_KEY;

if (!apiKey) {
throw new Error("Stripe secret key is missing!");
}

const stripe = new Stripe(apiKey);

const session = await stripe.checkout.sessions.create({
mode: "payment",
success_url: `${process.env.NEXT_PUBLIC_BASE_URL}/status?payment=success`,
cancel_url: `${process.env.NEXT_PUBLIC_BASE_URL}/status?payment=failed`,
line_items: [
{
price_data: {
currency: "USD",
product_data: {
name: product.name,
images: [`${process.env.NEXT_PUBLIC_BASE_URL}${product.image}`],
},
unit_amount: (product.price - product.price * (product.discount / 100)) * 100, // Stripe requires the price in cents so we multiply by 100
},
quantity,
},
],
});

// You do database operations here to save the session.id and other details
console.log("Session created", session.id);
return { sessionId: session.id };
} catch (error) {
console.log("Error creating checkout session", error);
return { error: "Error creating checkout session" };
}
}
4 changes: 3 additions & 1 deletion app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import type { Metadata } from "next";
import { Inter } from "next/font/google";
import "./globals.css";
import { Analytics } from "@vercel/analytics/react";
import { ToastContainer } from "react-toastify";

const inter = Inter({ subsets: ["latin"] });

Expand All @@ -17,8 +18,9 @@ export default function RootLayout({
}>) {
return (
<html lang="en">
<body className={inter.className}>
<body className={`${inter.className} h-min-screen`}>
{children}
<ToastContainer />
<Analytics />
</body>
</html>
Expand Down
58 changes: 20 additions & 38 deletions app/page.tsx
Original file line number Diff line number Diff line change
@@ -1,34 +1,12 @@
import Image from "next/image";
import Link from "next/link";

const paymentGateways = [
{
name: "Stripe",
description:
"Integrate Stripe's secure and flexible payment processing for global transactions.",
href: "/stripe",
},
{
name: "Razorpay",
description: "Implement Razorpay for seamless payments tailored for the Indian market.",
href: "/razorpay",
},
{
name: "Paypal",
description: "Set up PayPal to offer a trusted payment option recognized worldwide.",
href: "/paypal",
},
{
name: "Coinbase",
description: "Integrate Coinbase Commerce to accept cryptocurrency payments securely.",
href: "/coinbase",
},
];
import gateways from "@/data/gateways.json";

export default function Home() {
return (
<main className="flex min-h-screen flex-col items-center justify-between p-24">
<div className="z-10 w-full max-w-5xl items-center justify-between font-mono text-sm lg:flex">
<main className="flex py-16 com flex-col items-center justify-between container mx-auto">
<div className="z-10 px-4 w-full items-center justify-between font-mono text-sm lg:flex">
<p className="fixed left-0 top-0 flex w-full justify-center border-b border-gray-300 bg-gradient-to-b from-zinc-200 pb-6 pt-8 backdrop-blur-2xl dark:border-neutral-800 dark:bg-zinc-800/30 dark:from-inherit lg:static lg:w-auto lg:rounded-xl lg:border lg:bg-gray-200 lg:p-4 lg:dark:bg-zinc-800/30">
Souce code available at&nbsp;
<Link
Expand All @@ -52,7 +30,7 @@ export default function Home() {
</div>
</div>

<div className="relative m-5 z-[-1] flex place-items-center before:absolute before:h-[300px] before:w-full before:-translate-x-1/2 before:rounded-full before:bg-gradient-radial before:from-white before:to-transparent before:blur-2xl before:content-[''] after:absolute after:-z-20 after:h-[180px] after:w-full after:translate-x-1/3 after:bg-gradient-conic after:from-sky-200 after:via-blue-200 after:blur-2xl after:content-[''] before:dark:bg-gradient-to-br before:dark:from-transparent before:dark:to-blue-700 before:dark:opacity-10 after:dark:from-sky-900 after:dark:via-[#0141ff] after:dark:opacity-40 sm:before:w-[480px] sm:after:w-[240px] before:lg:h-[360px]">
<div className="relative mt-10 z-[-1] flex place-items-center before:absolute before:h-[300px] before:w-full before:-translate-x-1/2 before:rounded-full before:bg-gradient-radial before:from-white before:to-transparent before:blur-2xl before:content-[''] after:absolute after:-z-20 after:h-[180px] after:w-full after:translate-x-1/3 after:bg-gradient-conic after:from-sky-200 after:via-blue-200 after:blur-2xl after:content-[''] before:dark:bg-gradient-to-br before:dark:from-transparent before:dark:to-blue-700 before:dark:opacity-10 after:dark:from-sky-900 after:dark:via-[#0141ff] after:dark:opacity-40 sm:before:w-[480px] sm:after:w-[240px] before:lg:h-[360px]">
<Image
className="relative dark:drop-shadow-lg rounded-full"
src="/images/profile.png"
Expand All @@ -63,21 +41,25 @@ export default function Home() {
/>
</div>

<div className="mb-32 mt-24 grid text-center gap-4 lg:mb-0 lg:w-full lg:max-w-5xl lg:grid-cols-4 lg:text-left">
{paymentGateways.map((gateway, index) => (
<Link
<div className="mb-32 mt-20 px-4 grid text-center gap-4 lg:mb-0 w-full sm:grid-cols-2 lg:grid-cols-3 sm:text-left">
{gateways.map((gateway, index) => (
<div
key={index}
href={gateway.href}
className="group rounded-lg border border-transparent px-5 py-4 transition-colors hover:border-gray-300 hover:bg-gray-100 hover:dark:border-neutral-700 hover:dark:bg-neutral-800/30"
>
<h2 className="mb-3 text-2xl font-semibold">
{gateway.name}&nbsp;
<span className="inline-block transition-transform group-hover:translate-x-1 motion-reduce:transform-none">
-&gt;
</span>
</h2>
<p className="m-0 max-w-[30ch] text-sm opacity-50">{gateway.description}</p>
</Link>
<div>
<Link href={gateway.url}>
<h2 className="mb-3 text-2xl font-semibold">
{gateway.name}&nbsp;
<span className="inline-block transition-transform group-hover:translate-x-1 motion-reduce:transform-none">
-&gt;
</span>
</h2>
</Link>
</div>
<p className="m-0 text-sm opacity-50">{gateway.description}</p>
<Link href={gateway.website}></Link>
</div>
))}
</div>
</main>
Expand Down
74 changes: 74 additions & 0 deletions app/status/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import Link from "next/link";
import { redirect } from "next/navigation";

export default function Status({ searchParams }: { searchParams: { payment: string } }) {
const payment = searchParams.payment;

if (payment == "failed") {
return (
<div className="h-screen w-full flex items-center justify-center">
<div className=" bg-gray-200 dark:bg-gray-950 shadow-md rounded-lg p-6 md:mx-auto">
<svg
xmlns="http://www.w3.org/2000/svg"
className=" h-20 w-20 text-red-600 mx-auto my-6"
fill="currentColor"
viewBox="0 -8 528 528"
>
<title>fail</title>
<path d="M264 456Q210 456 164 429 118 402 91 356 64 310 64 256 64 202 91 156 118 110 164 83 210 56 264 56 318 56 364 83 410 110 437 156 464 202 464 256 464 310 437 356 410 402 364 429 318 456 264 456ZM264 288L328 352 360 320 296 256 360 192 328 160 264 224 200 160 168 192 232 256 168 320 200 352 264 288Z" />
</svg>

<div className="text-center">
<h3 className="md:text-2xl text-base text-gray-900 dark:text-gray-200 font-semibold text-center">
Payment Failed!
</h3>
<p className="text-gray-500 my-2">We are sorry, but your payment did not go through.</p>
<p>Please try again later.</p>
<div className="py-10 text-center">
<Link
href="/"
className="px-12 bg-blue-600 rounded-full shadow-md hover:bg-blue-500 text-white font-semibold py-3"
>
GO BACK
</Link>
</div>
</div>
</div>
</div>
);
}

if (payment == "success") {
return (
<div className="h-screen w-full flex items-center justify-center">
<div className=" bg-gray-200 dark:bg-gray-950 shadow-md rounded-lg p-6 md:mx-auto">
<svg viewBox="0 0 24 24" className="text-green-600 w-16 h-16 mx-auto my-6">
<path
fill="currentColor"
d="M12,0A12,12,0,1,0,24,12,12.014,12.014,0,0,0,12,0Zm6.927,8.2-6.845,9.289a1.011,1.011,0,0,1-1.43.188L5.764,13.769a1,1,0,1,1,1.25-1.562l4.076,3.261,6.227-8.451A1,1,0,1,1,18.927,8.2Z"
></path>
</svg>
<div className="text-center">
<h3 className="md:text-2xl text-base text-gray-900 dark:text-gray-200 font-semibold text-center">
Payment Done!
</h3>
<p className="text-gray-500 my-2">
Thank you for completing your secure online payment.
</p>
<p>Have a great day!</p>
<div className="py-10 text-center">
<Link
href="/"
className="px-12 bg-blue-600 rounded-full shadow-md hover:bg-blue-500 text-white font-semibold py-3"
>
GO BACK
</Link>
</div>
</div>
</div>
</div>
);
}

return redirect("/");
}
105 changes: 103 additions & 2 deletions app/stripe/page.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,108 @@
"use client";
import Image from "next/image";
import products from "@/data/products.json";
import { useTransition } from "react";
import { createCheckoutSession } from "@/actions/stripe";
import { loadStripe } from "@stripe/stripe-js";
import { toast } from "react-toastify";

export default function Stripe() {
const [loading, startTransition] = useTransition();

const product = products[0];

function handleBuy() {
startTransition(async () => {
const result = await createCheckoutSession({ productId: product.id, quantity: 1 });

if (!result || result.error || !result.sessionId) {
toast.error("Error creating checkout session");
return;
}

const publishableKey = process.env.NEXT_PUBLIC_STRIPE_PUBLIC_KEY;
if (!publishableKey) {
toast.error("Stripe public key is missing!");
return;
}
const stripe = await loadStripe(publishableKey);

if (!stripe) {
toast.error("Error loading Stripe");
return;
}

const { error } = await stripe.redirectToCheckout({
sessionId: result.sessionId,
});

if (error) {
toast.error("Error redirecting to checkout");
}
});
}

return (
<div>
<h1>Stripe</h1>
<div className="h-screen w-full flex justify-center items-center">
<div className="flex w-full max-w-xs flex-col overflow-hidden rounded-lg bg-white dark:bg-gray-950 shadow-md">
<div className="relative m-2 flex h-60 overflow-hidden rounded-xl">
<Image
height={500}
width={500}
className="object-cover"
src={product.image}
alt="product image"
/>
<span className="absolute top-0 left-0 m-2 rounded-full bg-black px-2 text-center text-sm font-medium text-white">
{product.discount}% OFF
</span>
</div>
<div className="mt-4 px-5 pb-5">
<a href="#">
<h5 className="text-xl tracking-tight text-slate-900 dark:text-gray-200">
{product.name}
</h5>
</a>
<div className="mt-2 mb-5 flex items-center justify-between">
<p>
<span className="text-3xl font-bold text-slate-900 dark:text-gray-200">
${product.price - product.price * (product.discount / 100)}
</span>
<span className="text-sm text-slate-900 dark:text-gray-200 line-through">
${product.price}
</span>
</p>
<div className="flex items-center">
{[...Array(product.rating)].map((_, index) => (
<svg
key={index}
aria-hidden="true"
className="h-5 w-5 text-yellow-500 "
fill="currentColor"
viewBox="0 0 20 20"
xmlns="http://www.w3.org/2000/svg"
>
<path d="M9.049 2.927c.3-.921 1.603-.921 1.902 0l1.07 3.292a1 1 0 00.95.69h3.462c.969 0 1.371 1.24.588 1.81l-2.8 2.034a1 1 0 00-.364 1.118l1.07 3.292c.3.921-.755 1.688-1.54 1.118l-2.8-2.034a1 1 0 00-1.175 0l-2.8 2.034c-.784.57-1.838-.197-1.539-1.118l1.07-3.292a1 1 0 00-.364-1.118L2.98 8.72c-.783-.57-.38-1.81.588-1.81h3.461a1 1 0 00.951-.69l1.07-3.292z" />
</svg>
))}

<span
className="mr-2 ml-3 rounded bg-yellow-500 text-white
px-2.5 py-0.5 text-xs font-semibold"
>
{product.rating}.0
</span>
</div>
</div>
<button
disabled={loading}
onClick={handleBuy}
className="w-full rounded-md bg-slate-900 px-5 py-2.5 text-center text-sm font-medium text-white hover:bg-gray-700 focus:outline-none focus:ring-4 focus:ring-blue-300"
>
{loading ? "Loading..." : "Hire Me"}
</button>
</div>
</div>
</div>
);
}
Loading

0 comments on commit 1af0758

Please sign in to comment.