Skip to content

Commit 63762cf

Browse files
committed
Added cashfree payment gateway
1 parent 6d5bc72 commit 63762cf

File tree

9 files changed

+572
-8
lines changed

9 files changed

+572
-8
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
1+
"use client";
2+
3+
import { useEffect, useState } from "react";
4+
import { useRouter, useSearchParams } from "next/navigation";
5+
import axios from "axios";
6+
import { BeatLoader } from "react-spinners";
7+
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
8+
import Link from "next/link";
9+
import { toast } from "sonner";
10+
export default function PaymentSuccess() {
11+
const router = useRouter();
12+
const searchParams = useSearchParams();
13+
const orderId = searchParams.get("order_id");
14+
const [status, setStatus] = useState<string>("");
15+
const [isLoading, setIsLoading] = useState<boolean>(false);
16+
17+
useEffect(() => {
18+
if (!orderId) {
19+
setStatus("FAILED");
20+
toast.error(
21+
<span className="text-rose-400">
22+
Order ID not found. Please contact support.
23+
</span>
24+
);
25+
return;
26+
}
27+
28+
const verifyPayment = async () => {
29+
setIsLoading(true);
30+
31+
try {
32+
const res = await axios.post("/api/verify-payment", {
33+
order_id: orderId,
34+
});
35+
console.log("Data from verify-payment: ", res.data);
36+
37+
if (res?.data?.success && res?.data?.data?.order_status === "PAID") {
38+
setStatus("VERIFIED");
39+
setIsLoading(false);
40+
toast.message(
41+
<span className="text-rose-400">
42+
✅ Payment Verified! Redirecting to Dashboard...
43+
</span>,
44+
{
45+
description: (
46+
<span className="text-slate-500">
47+
We have verified your payment.
48+
</span>
49+
),
50+
}
51+
);
52+
setTimeout(() => router.push("/dashboard"), 2000);
53+
} else {
54+
setStatus("FAILED");
55+
setIsLoading(false);
56+
toast.error("Payment verification failed. Please contact support.");
57+
setTimeout(() => router.push("/dashboard"), 5000);
58+
}
59+
} catch (error) {
60+
setStatus("FAILED");
61+
toast.error(
62+
<span className="text-rose-400">
63+
Payment verification failed. Please contact us.
64+
</span>
65+
);
66+
67+
let errorMessage =
68+
"Something went wrong! Please try again or contact support ⚠️";
69+
70+
if (axios.isAxiosError(error)) {
71+
if (error.response) {
72+
// Handle specific HTTP error codes
73+
if (error.response.status === 404) {
74+
errorMessage = "Order not found. Please check your order ID.";
75+
} else if (error.response.status === 400) {
76+
errorMessage = "Invalid request. Please try again.";
77+
} else if (error.response.status === 401) {
78+
errorMessage = "Authentication failed. Please contact support.";
79+
} else {
80+
errorMessage = "Payment verification failed. Please try again.";
81+
}
82+
} else if (error.request) {
83+
errorMessage =
84+
"Network issue! Unable to connect to the server. Please check your internet connection.";
85+
} else {
86+
errorMessage = "Unexpected error occurred while verifying payment.";
87+
}
88+
}
89+
setIsLoading(false);
90+
91+
setStatus("FAILED");
92+
toast.error(<span className="text-rose-400">{errorMessage}</span>);
93+
setTimeout(() => router.push("/dashboard"), 5000);
94+
} finally {
95+
setIsLoading(false);
96+
}
97+
};
98+
99+
verifyPayment();
100+
}, [orderId]);
101+
102+
return (
103+
<div className="flex p-4 flex-col items-center justify-start pt-12 sm:pt-24 min-h-screen">
104+
<Card className="w-full max-w-md flex flex-col space-y-4 p-4 py-10">
105+
<CardHeader>
106+
<h3 className="text-2xl md:text-3xl text-center bg-linear-to-r from-slate-600 via-rose-500 to-orange-600 bg-clip-text text-transparent font-bold">
107+
Verifying Payment
108+
</h3>
109+
<hr />
110+
</CardHeader>
111+
<CardContent className="space-y-4">
112+
<div className="w-full text-center">
113+
{status === "VERIFIED" ? (
114+
<div className="space-y-4">
115+
<h3 className="text-xl font-medium text-green-600">
116+
Your Payment has been verified successfully.
117+
</h3>
118+
<p className="text-muted-foreground text-sm">
119+
You will be redirected to your dashboard shortly.
120+
</p>
121+
</div>
122+
) : status === "FAILED" ? (
123+
<div className="space-y-4">
124+
<h3 className="text-xl font-medium text-rose-600">
125+
Payment Verification Failed.
126+
</h3>
127+
<p className="text-muted-foreground text-sm">
128+
You will be redirected to your dashboard shortly.
129+
</p>
130+
</div>
131+
) : (
132+
<BeatLoader color="#de407e" size={20} />
133+
)}
134+
</div>
135+
{isLoading && (
136+
<h4 className="text-sm text-rose-500 animate-pulse font-semibold text-center">
137+
Hold on for some time
138+
</h4>
139+
)}
140+
<p className="text-base text-gray-600 font-medium text-center bg-gradient-to-r from-slate-100 to-emerald-50 px-2 py-3 rounded-lg shadow-md hover:shadow-2xl transition-all duration-300">
141+
Once your payment is being verified, you'll be redirected to your
142+
dashboard. If not then click{" "}
143+
<span className="text-indigo-600 underline cursor-pointer">
144+
<Link href={"/dashboard"}>here</Link>
145+
</span>
146+
</p>
147+
</CardContent>
148+
</Card>
149+
</div>
150+
);
151+
}

app/(authenticated)/payment/page.tsx

+103
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
"use client";
2+
3+
import { useEffect, useState } from "react";
4+
import { useSearchParams } from "next/navigation";
5+
// @ts-ignore
6+
import { load } from "@cashfreepayments/cashfree-js";
7+
8+
export default function PaymentPage() {
9+
const searchParams = useSearchParams();
10+
const priceParam = searchParams.get("price");
11+
const plan = searchParams.get("plan");
12+
13+
const price = Number(priceParam);
14+
const [paymentSessionId, setPaymentSessionId] = useState<string | null>(null);
15+
const [loading, setLoading] = useState<boolean>(true);
16+
const [error, setError] = useState<string | null>(null);
17+
const [cashfree, setCashfree] = useState<any>(null);
18+
19+
useEffect(() => {
20+
const createPaymentSession = async () => {
21+
if (!price || isNaN(price) || !plan) {
22+
setError("Invalid price or missing plan.");
23+
setLoading(false);
24+
return;
25+
}
26+
27+
try {
28+
const response = await fetch("/api/payment", {
29+
method: "POST",
30+
headers: { "Content-Type": "application/json" },
31+
body: JSON.stringify({
32+
order_id: `order_${Date.now()}`,
33+
order_amount: price,
34+
customer_id: `cust_${Date.now()}`,
35+
customer_phone: "1234567890",
36+
return_url: `${
37+
window.location.origin
38+
}/payment-success?order_id=order_${Date.now()}`,
39+
}),
40+
});
41+
42+
const data = await response.json();
43+
console.log("Cashfree API Response:", data);
44+
45+
if (data.data?.payment_session_id) {
46+
setPaymentSessionId(data.data.payment_session_id);
47+
} else {
48+
throw new Error(data.error || "Payment initiation failed.");
49+
}
50+
} catch (error) {
51+
console.error("Payment initiation error:", error);
52+
setError(error instanceof Error ? error.message : "Unexpected error.");
53+
} finally {
54+
setLoading(false);
55+
}
56+
};
57+
58+
createPaymentSession();
59+
}, [price, plan]);
60+
61+
// Load Cashfree SDK once when component mounts
62+
useEffect(() => {
63+
const initializeCashfree = async () => {
64+
try {
65+
console.log("🚀 Initializing Cashfree...");
66+
const cashfreeInstance = await load({
67+
mode:
68+
process.env.NEXT_PUBLIC_CASHFREE_MODE === "TEST"
69+
? "sandbox"
70+
: "production",
71+
});
72+
setCashfree(cashfreeInstance);
73+
} catch (error) {
74+
console.error("❌ Cashfree SDK error:", error);
75+
setError("Error loading Cashfree SDK.");
76+
}
77+
};
78+
79+
initializeCashfree();
80+
}, []);
81+
82+
// Trigger payment when session ID is available
83+
useEffect(() => {
84+
if (!paymentSessionId || !cashfree) return;
85+
86+
console.log("✅ Cashfree SDK loaded. Starting payment...");
87+
88+
cashfree.checkout({
89+
paymentSessionId,
90+
redirectTarget: "_self", // Open in the same tab
91+
});
92+
}, [paymentSessionId, cashfree]);
93+
94+
return (
95+
<div className="flex flex-col items-center justify-center min-h-screen bg-gray-100">
96+
<h1 className="text-2xl font-bold mb-4">Payment for {plan}</h1>
97+
<p className="text-lg mb-4">Amount: ₹{price}</p>
98+
99+
{loading && <p>Loading payment gateway...</p>}
100+
{error && <p className="text-red-500">{error}</p>}
101+
</div>
102+
);
103+
}

app/api/payment/route.ts

+70
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
import { NextResponse } from "next/server";
2+
import { USE_BASE_URL } from "@/utils/use-base-url";
3+
4+
export async function POST(req: Request) {
5+
try {
6+
const { order_id, order_amount, customer_id, customer_phone } =
7+
await req.json();
8+
9+
console.log("Received payment request:", {
10+
order_id,
11+
order_amount,
12+
customer_id,
13+
customer_phone,
14+
});
15+
16+
const cashfreeEndpoint =
17+
process.env.NEXT_PUBLIC_CASHFREE_MODE === "TEST"
18+
? "https://sandbox.cashfree.com/pg/orders"
19+
: "https://api.cashfree.com/pg/orders";
20+
21+
console.log("Cashfree API Endpoint:", cashfreeEndpoint);
22+
23+
console.log("next public base url: ", USE_BASE_URL);
24+
25+
const response = await fetch(cashfreeEndpoint, {
26+
method: "POST",
27+
headers: {
28+
"Content-Type": "application/json",
29+
"x-client-id": process.env.CASHFREE_APP_ID!,
30+
"x-client-secret": process.env.CASHFREE_SECRET_KEY!,
31+
"x-api-version": "2025-01-01",
32+
},
33+
body: JSON.stringify({
34+
order_id,
35+
order_amount: Number(order_amount),
36+
order_currency: "USD",
37+
customer_details: {
38+
customer_id,
39+
customer_phone,
40+
customer_email: "john.doe@example.com",
41+
},
42+
order_meta: {
43+
return_url: `${USE_BASE_URL}/payment-success?order_id=${order_id}`,
44+
},
45+
}),
46+
});
47+
48+
const data = await response.json();
49+
console.log("Full Cashfree response:", JSON.stringify(data, null, 2));
50+
51+
if (data?.payment_session_id) {
52+
return NextResponse.json({
53+
success: true,
54+
data: { payment_session_id: data.payment_session_id },
55+
});
56+
} else {
57+
console.log("Error in Cashfree response:", data);
58+
return NextResponse.json({
59+
success: false,
60+
error: data.error || data.message || "Unknown error from Cashfree",
61+
});
62+
}
63+
} catch (error) {
64+
console.error("Error creating payment session:", error);
65+
return NextResponse.json({
66+
success: false,
67+
error: "Internal Server Error",
68+
});
69+
}
70+
}

app/api/verify-payment/route.ts

+53
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
import { USE_PAYMENT_MODE } from "@/utils/use-base-url";
2+
import { NextRequest, NextResponse } from "next/server";
3+
4+
export async function POST(req: NextRequest) {
5+
try {
6+
const { order_id } = await req.json();
7+
8+
if (!order_id) {
9+
return NextResponse.json({ error: "Missing order_id" }, { status: 400 });
10+
}
11+
12+
console.log("Verifying order:", order_id);
13+
14+
const cashfreeEndpoint =
15+
USE_PAYMENT_MODE === "TEST"
16+
? `https://sandbox.cashfree.com/pg/orders/${order_id}`
17+
: `https://api.cashfree.com/pg/orders/${order_id}`;
18+
19+
console.log("Cashfree App ID:", process.env.CASHFREE_APP_ID);
20+
console.log("Cashfree Secret Key:", process.env.CASHFREE_SECRET_KEY);
21+
console.log("Cashfree Mode:", process.env.CASHFREE_MODE);
22+
23+
const response = await fetch(cashfreeEndpoint, {
24+
method: "GET",
25+
headers: {
26+
"Content-Type": "application/json",
27+
"x-client-id": process.env.CASHFREE_APP_ID!,
28+
"x-client-secret": process.env.CASHFREE_SECRET_KEY!,
29+
"x-api-version": "2023-08-01",
30+
},
31+
});
32+
33+
if (!response.ok) {
34+
const errorData = await response.json();
35+
console.log("❌ Cashfree API Error:", errorData);
36+
return NextResponse.json(
37+
{ error: errorData.message || "Failed to fetch order" },
38+
{ status: response.status }
39+
);
40+
}
41+
42+
const data = await response.json();
43+
console.log("✅ Cashfree API Response:", data);
44+
45+
return NextResponse.json({ success: true, data });
46+
} catch (error) {
47+
console.error("❌ Internal Server Error:", error);
48+
return NextResponse.json(
49+
{ error: "Internal Server Error" },
50+
{ status: 500 }
51+
);
52+
}
53+
}

0 commit comments

Comments
 (0)