I want to integrate PayPal webhook with my next JS project. I have been stuck here for the last three days. order create and capture order is working. but handle approve API is not working. I am unable to find where that error. because I am using webhook for the first time. So, I don’t have much idea about it. Can anyone help me?
component/Checkout.tsx:
"use client";
import { useToast } from "@/components/ui/use-toast";
import {
FUNDING,
PayPalButtons,
PayPalScriptProvider,
} from "@paypal/react-paypal-js";
import React, { useState } from "react";
import { Button } from "../ui/button";
interface PromoCodes {
[key: string]: string;
}
const Checkout = ({
plan,
amount,
credits,
buyerId,
}: {
plan: string;
amount: number;
credits: number;
buyerId: string;
}) => {
const { toast } = useToast();
const [promoCode, setPromoCode] = useState("");
const [discountedAmount, setDiscountedAmount] = useState(amount);
const [isPromoCodeValid, setIsPromoCodeValid] = useState(true);
const [errors, setErrors] = useState("");
const promoCodes: PromoCodes = {
DISCOUNT25: "https://www.sandbox.paypal.com/ncp/payment/F6JZVGEAL5L6C",
// Add more promo codes and corresponding checkout links here
};
const handlePromoCodeChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setPromoCode(e.target.value);
};
const applyPromoCode = () => {
if (promoCodes[promoCode]) {
setDiscountedAmount(amount * 0.75);
setIsPromoCodeValid(true);
toast({
title: "Promo code applied!",
description: "You received a 25% discount.",
duration: 5000,
className: "success-toast",
});
} else {
setIsPromoCodeValid(false);
toast({
title: "Invalid promo code",
description: "Please try a different code.",
duration: 5000,
className: "error-toast",
});
}
};
const [orderID, setOrderID] = useState(null);
const handleApprove = async (orderID: any) => {
try {
const response = await fetch("/api/webhooks/paypal", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ orderID, buyerId, credits }),
});
const data = await response.json();
if (data.success) {
alert("Payment successful and credits updated");
} else {
alert("Payment failed");
}
} catch (error) {
console.error("Error:", error);
alert("An error occurred");
}
};
return (
<div>
<div className="flex flex-col gap-4">
<h2 className="text-xl font-bold">{plan}</h2>
<p className="text-lg">Credits: {credits}</p>
<p className="text-lg">Price: ${discountedAmount.toFixed(2)}</p>
{plan !== "Free" && (
<>
<input
type="text"
placeholder="Enter promo code"
value={promoCode}
onChange={handlePromoCodeChange}
className={`border rounded p-2 ${
isPromoCodeValid ? "" : "border-red-500"
}`}
/>
<Button
onClick={applyPromoCode}
className="w-full rounded bg-purple-600 text-white"
>
Apply Promo Code
</Button>
<PayPalScriptProvider
options={{ clientId: process.env.NEXT_PUBLIC_PAYPAL_CLIENT_ID! }}
>
<PayPalButtons
fundingSource={FUNDING.PAYPAL}
createOrder={(data, actions) => {
return actions.order.create({
intent: "CAPTURE",
purchase_units: [
{
description: plan,
amount: {
currency_code: "USD",
value: discountedAmount.toFixed(2),
},
custom_id: JSON.stringify({ plan, credits, buyerId }),
},
],
});
}}
onApprove={(data: any, actions: any) => {
return actions?.order?.capture()?.then((details: any) => {
handleApprove(data.orderID);
});
}}
onError={(err) => {
toast({
title: "Error",
description: "An error occurred during payment processing.",
duration: 5000,
className: "error-toast",
});
}}
/>
</PayPalScriptProvider>
</>
)}
</div>
</div>
);
};
export default Checkout;
here is the api/webhooks/paypal/route.ts:
import { updateCredits } from "@/lib/actions/user.actions";
import Transaction from "@/lib/database/models/transaction.model";
import { connectToDatabase } from "@/lib/database/mongoose";
import { handleError } from "@/lib/utils";
import crypto from "crypto";
import { NextApiRequest, NextApiResponse } from "next";
const webhookId = process.env.PAYPAL_WEBHOOK_ID!;
const paypalClientId = process.env.PAYPAL_CLIENT_ID!;
const paypalSecret = process.env.PAYPAL_SECRET!;
export default async function handler(
req: NextApiRequest,
res: NextApiResponse
) {
if (req.method !== "POST") {
res.setHeader("Allow", "POST");
res.status(405).end("Method Not Allowed");
return;
}
// Extract PayPal headers
const transmissionId = req.headers["paypal-transmission-id"] as string;
const transmissionTime = req.headers["paypal-transmission-time"] as string;
const certUrl = req.headers["paypal-cert-url"] as string;
const authAlgo = req.headers["paypal-auth-algo"] as string;
const transmissionSig = req.headers["paypal-transmission-sig"] as string;
// Get the body and parse it
const body = JSON.stringify(req.body);
// Verify the webhook signature
const expectedSignature = crypto
.createHmac("sha256", webhookId)
.update(
transmissionId + "|" + transmissionTime + "|" + body + "|" + webhookId
)
.digest("hex");
if (expectedSignature !== transmissionSig) {
res.status(400).json({ error: "Invalid signature" });
return;
}
const { event_type, resource } = req.body;
try {
if (event_type === "PAYMENT.SALE.COMPLETED") {
const { id, amount, custom } = resource;
const { plan, credits, buyerId } = JSON.parse(custom);
await connectToDatabase();
// Create a new transaction
const newTransaction = await Transaction.create({
createdAt: new Date(),
paypalId: id,
amount: parseFloat(amount.total),
plan,
credits,
buyer: buyerId,
});
// Update the user's credits
await updateCredits(buyerId, credits);
res.status(200).json({ success: true });
} else {
res.status(200).json({ message: "Event type not handled" });
}
} catch (error) {
handleError(error);
res.status(500).json({ error: "Internal Server Error" });
}
}
user.action.ts file:
// USE CREDITS
export async function updateCredits(userId: string, creditFee: number) {
try {
await connectToDatabase();
const updatedUserCredits = await User.findOneAndUpdate(
{ clerkId: userId },
{ $inc: { creditBalance: creditFee } },
{ new: true }
);
if (!updatedUserCredits) throw new Error("User credits update failed");
return JSON.parse(JSON.stringify(updatedUserCredits));
} catch (error) {
handleError(error);
}
}
Here is credit page:
import { SignedIn, auth } from "@clerk/nextjs";
import Image from "next/image";
import { redirect } from "next/navigation";
import Checkout from "@/components/shared/Checkout";
import Header from "@/components/shared/Header";
import { Button } from "@/components/ui/button"; // Ensure the Button component is imported
import { plans as creditPlans } from "@/constants"; // Renaming plans during import
import { getUserById } from "@/lib/actions/user.actions";
const Credits = async () => {
const { userId } = auth();
if (!userId) redirect("/sign-in");
const user = await getUserById(userId);
return (
<>
<Header
title="Buy Credits"
subtitle="Choose a credit package that suits your needs!"
/>
<section>
<ul className="credits-list">
{creditPlans.map((plan) => (
<li key={plan.name} className="credits-item">
<div className="flex-center flex-col gap-3">
<Image
src={plan.icon}
alt="credit icon"
width={50}
height={50}
/>
<p className="p-20-semibold mt-2 text-purple-500">
{plan.name}
</p>
<p className="h1-semibold text-dark-600">${plan.price}</p>{" "}
{/* Correct price display */}
<p className="p-16-regular">{plan.credits} Credits</p>
</div>
{plan.name === "Free" ? (
<Button variant="outline" className="credits-btn">
Free Consumable
</Button>
) : (
<SignedIn>
<Checkout
plan={plan.name}
amount={plan.price}
credits={plan.credits}
buyerId={user.clerkId}
/>
</SignedIn>
)}
</li>
))}
</ul>
</section>
</>
);
};
export default Credits;
I want to update my user credit when user pay with paypal.