diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..bd2e2bc --- /dev/null +++ b/.env.example @@ -0,0 +1,15 @@ +# Razorpay +NEXT_PUBLIC_RAZORPAY_KEY_ID=your_razorpay_key_id +RAZORPAY_KEY_ID=your_razorpay_key_id +RAZORPAY_KEY_SECRET=your_razorpay_secret + +# Supabase +NEXT_PUBLIC_SUPABASE_URL=https://your-project.supabase.co +NEXT_PUBLIC_SUPABASE_ANON_KEY=your_supabase_anon_key + +# SMTP / Email +SMTP_HOST=smtp.gmail.com +SMTP_PORT=465 +SMTP_USER=your_email@gmail.com +SMTP_PASS=your_app_password +EMAIL_FROM="AsHelp " diff --git a/.gitignore b/.gitignore index c325023..2515836 100644 --- a/.gitignore +++ b/.gitignore @@ -41,4 +41,4 @@ yarn-error.log* *.tsbuildinfo next-env.d.ts -.eslintcache \ No newline at end of file +.eslintcache.env.local diff --git a/package-lock.json b/package-lock.json index f5a401b..97afd30 100644 --- a/package-lock.json +++ b/package-lock.json @@ -31,6 +31,7 @@ "motion": "^12.23.24", "next": "^15.5.4", "next-themes": "^0.4.6", + "nodemailer": "^7.0.9", "ogl": "^1.0.11", "radix-ui": "^1.4.2", "razorpay": "^2.9.6", @@ -7184,6 +7185,15 @@ "dev": true, "license": "MIT" }, + "node_modules/nodemailer": { + "version": "7.0.9", + "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-7.0.9.tgz", + "integrity": "sha512-9/Qm0qXIByEP8lEV2qOqcAW7bRpL8CR9jcTwk3NBnHJNmP9fIJ86g2fgmIXqHY+nj55ZEMwWqYAT2QTDpRUYiQ==", + "license": "MIT-0", + "engines": { + "node": ">=6.0.0" + } + }, "node_modules/normalize-range": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/normalize-range/-/normalize-range-0.1.2.tgz", diff --git a/package.json b/package.json index 90f6bac..bc8be38 100644 --- a/package.json +++ b/package.json @@ -34,6 +34,7 @@ "motion": "^12.23.24", "next": "^15.5.4", "next-themes": "^0.4.6", + "nodemailer": "^7.0.9", "ogl": "^1.0.11", "radix-ui": "^1.4.2", "razorpay": "^2.9.6", diff --git a/src/app/api/notify/confirmation/route.ts b/src/app/api/notify/confirmation/route.ts new file mode 100644 index 0000000..21b2c92 --- /dev/null +++ b/src/app/api/notify/confirmation/route.ts @@ -0,0 +1,32 @@ +import { NextResponse } from "next/server"; +import { sendOrderEmail } from "@/lib/mailer"; + +export async function POST(req: Request) { + try { + const { customerEmail, customerName, orderId, paymentId, items, totalAmount, isCOD } = await req.json(); + if (!customerEmail || !orderId) { + return NextResponse.json({ error: "customerEmail and orderId required" }, { status: 400 }); + } + + try { + await sendOrderEmail({ + to: customerEmail, + customerName: customerName ?? "Customer", + orderId, + paymentId: paymentId ?? (isCOD ? `COD-${orderId}` : "N/A"), + items: Array.isArray(items) ? items : [], + totalAmount: Number(totalAmount ?? 0), + isCOD: Boolean(isCOD), + }); + } catch (mailErr) { + console.error("[notify/confirmation] email failed:", mailErr, { orderId, customerEmail }); + // Persist to your DB for retry if desired. + } + + return NextResponse.json({ ok: true }); + } catch (err: any) { + console.error("[notify/confirmation] error:", err); + return NextResponse.json({ error: "notify_failed" }, { status: 500 }); + } +} + diff --git a/src/app/api/razorpay/create-order/route.ts b/src/app/api/razorpay/create-order/route.ts new file mode 100644 index 0000000..680dfa7 --- /dev/null +++ b/src/app/api/razorpay/create-order/route.ts @@ -0,0 +1,21 @@ +import { NextResponse } from "next/server"; +import { razorpay } from "@/lib/razorpay"; + +export async function POST(req: Request) { + try { + const { amount, receipt } = await req.json(); + // amount should be in INR paise (₹1 = 100) + if (!amount) return NextResponse.json({ error: "amount required" }, { status: 400 }); + + const order = await razorpay.orders.create({ + amount: Number(amount), + currency: "INR", + receipt: receipt ?? `rcpt_${Date.now()}`, + }); + + return NextResponse.json({ order }); + } catch (err: any) { + console.error("[create-order] error:", err); + return NextResponse.json({ error: "failed_to_create_order" }, { status: 500 }); + } +} diff --git a/src/app/api/razorpay/create-order/verify/route.ts b/src/app/api/razorpay/create-order/verify/route.ts new file mode 100644 index 0000000..5612c9a --- /dev/null +++ b/src/app/api/razorpay/create-order/verify/route.ts @@ -0,0 +1,56 @@ +import { NextResponse } from "next/server"; +import crypto from "crypto"; +import { sendOrderEmail } from "@/lib/mailer"; + +export async function POST(req: Request) { + try { + const body = await req.json(); + const { + razorpay_order_id, + razorpay_payment_id, + razorpay_signature, + // for email: + customerEmail, + customerName, + items, // [{name, qty}] + totalAmount, // in rupees (for email only) + orderIdPublic, // your own app order id (if you have one) + } = body; + + if (!razorpay_order_id || !razorpay_payment_id || !razorpay_signature) { + return NextResponse.json({ error: "missing_razorpay_params" }, { status: 400 }); + } + + const keySecret = process.env.RAZORPAY_KEY_SECRET as string; + const expected = crypto + .createHmac("sha256", keySecret) + .update(`${razorpay_order_id}|${razorpay_payment_id}`) + .digest("hex"); + + if (expected !== razorpay_signature) { + return NextResponse.json({ error: "invalid_signature" }, { status: 400 }); + } + + // Signature OK → send email + try { + await sendOrderEmail({ + to: customerEmail, + customerName, + orderId: orderIdPublic ?? razorpay_order_id, + paymentId: razorpay_payment_id, + items: Array.isArray(items) ? items : [], + totalAmount: Number(totalAmount ?? 0), + isCOD: false, + }); + } catch (mailErr) { + // Log email failure for retry + console.error("[verify] email send failed:", mailErr, { customerEmail, razorpay_order_id }); + // You can persist to DB here if you want a retry queue. + } + + return NextResponse.json({ ok: true }); + } catch (err: any) { + console.error("[verify] error:", err); + return NextResponse.json({ error: "verify_failed" }, { status: 500 }); + } +} diff --git a/src/components/payment.tsx b/src/components/payment.tsx index e65d21b..35bec52 100644 --- a/src/components/payment.tsx +++ b/src/components/payment.tsx @@ -1,68 +1,209 @@ "use client"; -import React, { useState, useEffect } from "react"; +import React, { useState, useMemo } from "react"; import { BackgroundBeamsWithCollision } from "@/components/ui/background-beams-with-collision"; import { Button } from "@/components/ui/button"; -import { DotLottieReact } from '@lottiefiles/dotlottie-react'; -import { useSearchParams } from 'next/navigation'; +import { DotLottieReact } from "@lottiefiles/dotlottie-react"; +import { useSearchParams } from "next/navigation"; import { GradientIconButton } from "@/components/ui/GradientIconButton"; import Script from "next/script"; +// Types for window.Razorpay +declare global { + interface Window { + Razorpay?: any; + } +} + export function PaymentOptionsOverlay() { const searchParams = useSearchParams(); - const amount = searchParams.get('amount') || '100'; - const [selected, setSelected] = useState("cod"); + + // ---- Read data from query params (with fallbacks) ---- + const amountStr = searchParams.get("amount") || "100"; + const amountRupees = Number(amountStr) || 100; + const customerName = searchParams.get("name") || "Customer"; + const customerEmail = searchParams.get("email") || "customer@example.com"; + // Your internal order id (e.g., assignment id). If you don't have one, we generate a temporary one. + const appOrderId = + searchParams.get("orderId") || `ASHELP-${new Date().getTime()}`; + + // Basic item model. You can expand this to match your actual cart. + const items = useMemo(() => { + const qpName = searchParams.get("itemName"); + const qpQty = Number(searchParams.get("qty") || "1"); + const pages = Number(searchParams.get("pages") || "0"); + // If pages are provided, show that as quantity; else default 1. + if (qpName) { + return [{ name: qpName, qty: qpQty > 0 ? qpQty : 1 }]; + } + if (pages > 0) { + return [{ name: "Assignment Pages", qty: pages }]; + } + return [{ name: "Assignment", qty: 1 }]; + }, [searchParams]); + + // ---- UI state ---- + const [selected, setSelected] = useState<"cod" | "online">("cod"); const [confirmed, setConfirmed] = useState(false); const [showThankYou, setShowThankYou] = useState(false); const [razorpayLoaded, setRazorpayLoaded] = useState(false); + const [busy, setBusy] = useState(false); - useEffect(() => { + // Show thank-you panel after the short animation + React.useEffect(() => { if (confirmed) { - const timer = setTimeout(() => { - setShowThankYou(true); - }, 2000); // 2 seconds for animation - return () => clearTimeout(timer); + const t = setTimeout(() => setShowThankYou(true), 2000); + return () => clearTimeout(t); } }, [confirmed]); - // Dynamically load Razorpay script - useEffect(() => { - if (!razorpayLoaded) { - const script = document.createElement("script"); - script.src = "https://checkout.razorpay.com/v1/checkout.js"; - script.async = true; - script.onload = () => setRazorpayLoaded(true); - document.body.appendChild(script); + const razorpayKey = process.env.NEXT_PUBLIC_RAZORPAY_KEY_ID; + + // --------------------------- + // COD: send confirmation email via server + // --------------------------- + const placeCOD = async () => { + if (busy) return; + setBusy(true); + try { + const res = await fetch("/api/notify/confirmation", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + customerEmail, + customerName, + orderId: appOrderId, + paymentId: `COD-${appOrderId}`, + items, + totalAmount: amountRupees, + isCOD: true, + }), + }); + const data = await res.json(); + if (!res.ok || !data?.ok) { + // Email failed (or route error). We still confirm the order, but we notify the user. + alert( + "Order placed. We couldn't send the email right now—no worries, we'll follow up." + ); + } + setConfirmed(true); + } catch (err) { + console.error("[COD] notify/confirmation failed:", err); + alert( + "Order placed. We couldn't send the email right now—no worries, we'll follow up." + ); + setConfirmed(true); + } finally { + setBusy(false); } - }, [razorpayLoaded]); + }; - const razorpayKey = process.env.NEXT_PUBLIC_RAZORPAY_KEY_ID; + // --------------------------- + // Razorpay: create order (server) → open checkout → verify (server) → email + // --------------------------- + const handleRazorpayPayment = async () => { + if (busy) return; + if (!razorpayLoaded || !window.Razorpay) { + alert("Payment system is still loading. Please try again in a moment."); + return; + } + if (!razorpayKey) { + alert("Razorpay key is not configured."); + return; + } - const handleRazorpayPayment = () => { - if (!razorpayLoaded) return; - const options = { - key: razorpayKey, - amount: Number(amount) * 100, // Amount in paise - currency: "INR", - name: "Your Company Name", - description: "Order Payment", - image: "/logo.png", // Optional: your logo - handler: function () { - // Payment successful - setConfirmed(true); - }, - prefill: { - name: "", - email: "", - contact: "", - }, - theme: { - color: "#3399cc", - }, - }; - // @ts-expect-error Razorpay is loaded dynamically - const rzp = new window.Razorpay(options); - rzp.open(); + setBusy(true); + try { + const amountPaise = Math.round(amountRupees * 100); + + // 1) Create order on server + const orderRes = await fetch("/api/razorpay/create-order", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + amount: amountPaise, + receipt: `ashelp_${Date.now()}`, + }), + }); + const { order, error } = await orderRes.json(); + if (!orderRes.ok || !order?.id) { + console.error("[create-order] error:", error || orderRes.statusText); + alert("Could not start payment. Please try again."); + setBusy(false); + return; + } + + // 2) Configure Razorpay options + const options = { + key: razorpayKey, + amount: amountPaise, + currency: "INR", + name: "AsHelp", + description: "Assignment/Order Payment", + image: "/logo.png", + order_id: order.id, // IMPORTANT + prefill: { + name: customerName, + email: customerEmail, + }, + notes: { + app_order_id: appOrderId, + }, + handler: async (response: { + razorpay_order_id: string; + razorpay_payment_id: string; + razorpay_signature: string; + }) => { + try { + // 3) Verify payment on server (also sends email) + const verifyRes = await fetch("/api/razorpay/verify", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + razorpay_order_id: response.razorpay_order_id, + razorpay_payment_id: response.razorpay_payment_id, + razorpay_signature: response.razorpay_signature, + // Email payload: + customerEmail, + customerName, + items, + totalAmount: amountRupees, + orderIdPublic: appOrderId, + }), + }); + const verifyJson = await verifyRes.json(); + if (!verifyRes.ok || !verifyJson?.ok) { + alert( + "Payment received. We couldn't send the email right now—no worries, we'll follow up." + ); + } + setConfirmed(true); + } catch (e) { + console.error("[verify] failed:", e); + alert( + "Payment received. We couldn't send the email right now—no worries, we'll follow up." + ); + setConfirmed(true); + } finally { + setBusy(false); + } + }, + theme: { color: "#6C5CE7" }, + modal: { + ondismiss: () => { + setBusy(false); + }, + }, + }; + + // 4) Open Razorpay + const rzp = new window.Razorpay(options); + rzp.open(); + } catch (err) { + console.error("[razorpay] error:", err); + alert("Something went wrong starting the payment. Please try again."); + setBusy(false); + } }; if (showThankYou) { @@ -70,35 +211,83 @@ export function PaymentOptionsOverlay() {
- Thank you for ordering from us -

What happens next?

+ + Thank you for ordering from us + +

+ What happens next? +

{/* Order Confirmation */}
- {/* Mail Icon */} - + + + + + + + + Order Confirmation + + + You'll receive an email confirmation shortly with your + order details. - Order Confirmation - You'll receive an email confirmation shortly with your order details.
{/* Processing */}
- {/* Box Icon */} - + + + + + + + Processing + + + We'll prepare your order for shipment within 1-2 business + days. - Processing - We'll prepare your order for shipment within 1-2 business days.
{/* Shipping */}
- {/* Truck Icon */} - + + + + + + + Shipping + + + Track your package with the tracking number we'll send + you. - Shipping - Track your package with the tracking number we'll send you.
@@ -107,14 +296,43 @@ export function PaymentOptionsOverlay() {
+ {/* Footer: Need Help? */}
- Need Help? + + Need Help? +
- fukutsu07@gmail.com - 6398317816 + + + + + {" "} + fukutsu07@gmail.com + + + + + + {" "} + 6398317816 +
- Customer service is available Monday-Friday, 9AM-6PM EST + + Customer service is available Monday-Friday, 9AM-6PM EST +
@@ -123,8 +341,13 @@ export function PaymentOptionsOverlay() { return ( <> - {/* Razorpay script for SSR safety */} -