From ccbf8539ba28cf5ca3ee12608346952226269282 Mon Sep 17 00:00:00 2001 From: nishantdotcom Date: Mon, 15 Sep 2025 23:59:34 +0530 Subject: [PATCH 1/2] Added payment modal --- frontend/app/(app)/pricing/page.tsx | 6 +- frontend/components/PaymentModal.tsx | 400 +++++++++++++++++++++++---- 2 files changed, 353 insertions(+), 53 deletions(-) diff --git a/frontend/app/(app)/pricing/page.tsx b/frontend/app/(app)/pricing/page.tsx index d4d02d5c..e3af420a 100644 --- a/frontend/app/(app)/pricing/page.tsx +++ b/frontend/app/(app)/pricing/page.tsx @@ -9,9 +9,9 @@ import { CheckIcon, StarIcon } from "@phosphor-icons/react/dist/ssr"; -import RazorpayPayment from "@/components/RazorpayPayment"; import { useEffect } from "react"; import { useCredits } from "@/hooks/useCredits"; +import PaymentModal from "@/components/PaymentModal"; const pricingPlans = [ { @@ -144,7 +144,7 @@ export default function PricingPage() { ))} - {plan.cta.text} - + ))} diff --git a/frontend/components/PaymentModal.tsx b/frontend/components/PaymentModal.tsx index 9aba9557..24a4c7d1 100644 --- a/frontend/components/PaymentModal.tsx +++ b/frontend/components/PaymentModal.tsx @@ -1,65 +1,365 @@ +"use client"; + import { BACKEND_URL } from "@/lib/utils"; -import React from "react"; +import React, { useState } from "react"; import { useRazorpay, RazorpayOrderOptions } from "react-razorpay"; import axios from "axios"; +import { toast } from "sonner"; import { Button } from "./ui/button"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, + DialogTrigger, +} from "./ui/dialog"; +import { Label } from "./ui/label"; +import { CreditCard, Wallet, Building } from "lucide-react"; +import { useUser } from "@/hooks/useUser"; +import { useCredits } from "@/hooks/useCredits"; +import { useRouter } from "next/navigation"; -const RZP_KEY = process.env.NEXT_PUBLIC_RZP_KEY ?? "rzp_live_haOcAMPhYa4O6r"; -const PaymentComponent = () => { - const { error, isLoading, Razorpay } = useRazorpay(); +const RZP_KEY = process.env.NEXT_PUBLIC_RZP_KEY ?? "rzp_test_5hzuC4lSlUBLqb"; + +type PaymentProvider = "razorpay" | "stripe" | "paddle"; + +interface PaymentModalProps { + children: React.ReactNode; + plan: { + name: string; + price: number; + currency: string; + interval: string; + }; + className?: string; +} + +const paymentProviders = [ + { + id: "razorpay" as PaymentProvider, + name: "Razorpay", + description: "Pay with UPI, Cards, Net Banking", + icon: Wallet, + popular: true, + }, + { + id: "stripe" as PaymentProvider, + name: "Stripe", + description: "Pay with Credit/Debit Cards", + icon: CreditCard, + popular: false, + }, + { + id: "paddle" as PaymentProvider, + name: "Paddle", + description: "International Payments", + icon: Building, + popular: false, + }, +]; + +const PaymentModal = ({ children, plan, className }: PaymentModalProps) => { + const { error , isLoading :rzpLoading , Razorpay } = useRazorpay(); + const [isOpen, setIsOpen] = useState(false); + const { user, isLoading: userLoading } = useUser(); + const { userCredits } = useCredits(); + const [isProcessing, setIsProcessing] = useState(false); + const router = useRouter(); + const [selectedProvider, setSelectedProvider] = useState("razorpay"); + + + const handleRazorpayPayment = async () => { + console.log("Initiating Razorpay payment for plan:", plan); + console.log("User provider:", user); + console.log("Selected payment provider:", userCredits); + + console.log("Razorpay instance:", Razorpay); + if (!user) { + toast.error("Please login to subscribe"); + router.push("/auth"); + return; + } + if (userCredits?.isPremium) { + toast.info("You're already subscribed to our premium plan!"); + return; + } + + try { + setIsProcessing(true); + toast.loading("Initializing payment...", { id: "payment-init" }); + const response = await axios.post(`${BACKEND_URL}/billing/init-subscribe`, { + planType: plan.name.toLowerCase() + }, { + headers: { + "Authorization": `Bearer ${localStorage.getItem("token")}` + } + }); + console.log("Razorpay order creation response:", response.data); + toast.dismiss("payment-init"); + if (response.data.orderId){ + const isYearlyPlan = plan.name.toLowerCase() === "yearly"; + if (isYearlyPlan) { + const options: RazorpayOrderOptions = { + key: response.data.rzpKey || RZP_KEY, + amount: response.data.amount * 100, + currency: response.data.currency, + name: "1AI", + description: `${plan.name} Plan - One-time Payment`, + order_id: response.data.orderId, + prefill: { + name: user.name || "User", + email: user.email || "", + }, + handler: async (response: any) => { + if (response.razorpay_payment_id) { + toast.success("Payment successful! Your yearly plan is being activated...", { + duration: 3000 + }); + + let signature = response.razorpay_signature; + let razorpay_payment_id = response.razorpay_payment_id; + + try { + console.log(BACKEND_URL); + console.log(response); + const response2 = await axios.post( + `${BACKEND_URL}/billing/verify-payment`, + { + signature, + razorpay_payment_id, + orderId: response.razorpay_order_id + }, + { + headers: { + "Authorization": `Bearer ${localStorage.getItem("token")}` + } + } + ); + + if (response2.data.success) { + toast.success(response2.data.message || "Payment successful! Your yearly plan is now active!", { + duration: 5000 + }); + window.location.reload(); + } else { + toast.error("Payment failed! Please try again."); + } + } catch (error: any) { + console.error("Error verifying payment:", error); + toast.error(error?.response?.data?.error || "Payment failed! Please try again."); + } + } + }, + modal: { + ondismiss: () => { + setIsProcessing(false); + toast.info("Payment cancelled"); + } + }, + theme: { + color: "#F37254", + }, + } ; + const razorpayInstance = new Razorpay(options as any); + razorpayInstance.open(); + - const handlePayment = async () => { - const response = await axios.post(`${BACKEND_URL}/billing/init-subscribe`, {}, { - headers: { - "Authorization": `Bearer ${localStorage.getItem("token")}` } - }); - - console.log(response.data); - alert(response.data.orderId) - - const options: RazorpayOrderOptions = { - key: RZP_KEY, - amount: 100 * 100, - currency: "INR", - name: "1AI", - description: "Montly Subscription", - order_id: response.data.orderId, - prefill: { - name: "John Doe", - email: "john.doe@example.com", - contact: "+919876543210", - }, - handler: (response) => { - if (response.razorpay_payment_id) { - axios.post(`${BACKEND_URL}/billing/subscribe`, { - orderId: response.razorpay_order_id, - paymentId: response.razorpay_payment_id, - signature: response.razorpay_signature, - }, { - headers: { - "Authorization": `Bearer ${localStorage.getItem("token")}` + else{ + + const options = { + key: response.data.rzpKey || RZP_KEY, + subscription_id: response.data.orderId, + name: "1AI", + description: `${plan.name} Subscription`, + prefill: { + name: user.name || "User", + email: user.email || "", + }, + handler: async (response: any) => { + // Payment successful + if (response.razorpay_payment_id) { + toast.success("Payment successful! Your subscription is being activated...", { + duration: 3000 + }); + + const signature = response.razorpay_signature; + const razorpay_payment_id = response.razorpay_payment_id; + + try { + const response2 = await axios.post( + `${BACKEND_URL}/billing/verify-payment`, + { + signature, + razorpay_payment_id, + orderId: response.razorpay_order_id + }, + { + headers: { + "Authorization": `Bearer ${localStorage.getItem("token")}` + } + } + ); + + if (response2.data.success) { + toast.success("Payment successful! Your subscription is being activated...", { + duration: 3000 + }); + // Refresh user credits + window.location.reload(); + } else { + toast.error("Payment failed! Please try again."); + } + } catch (error: any) { + console.error("Error verifying payment:", error); + toast.error(error?.response?.data?.error || "Payment failed! Please try again."); } - }); - } - }, - theme: { - color: "#F37254", - }, - }; - - const razorpayInstance = new Razorpay(options); - razorpayInstance.open(); + } + }, + modal: { + ondismiss: () => { + setIsProcessing(false); + toast.info("Payment cancelled"); + } + }, + theme: { + color: "#F37254", + + }, + }; + + const razorpayInstance = new Razorpay(options as any); + razorpayInstance.open(); + } + } else { + throw new Error("No subscription ID received from server"); + } + + } catch (error: any) { + console.error("Razorpay payment initialization failed:", error); + console.error("Error details:", error.response?.data); + alert(`Payment initialization failed: ${error.response?.data?.error || error.message}`); + } finally { + setIsProcessing(false); + } }; + const handleStripePayment = async () => { + // Placeholder for Stripe integration + console.log("Stripe payment selected"); + alert("Stripe integration coming soon!"); + }; + + const handlePaddlePayment = async () => { + // Placeholder for Paddle integration + console.log("Paddle payment selected"); + alert("Paddle integration coming soon!"); + }; + + const handlePayment = async () => { + switch (selectedProvider) { + case "razorpay": + await handleRazorpayPayment(); + break; + case "stripe": + await handleStripePayment(); + break; + case "paddle": + await handlePaddlePayment(); + break; + default: + console.error("Unknown payment provider:", selectedProvider); + } + }; + const isLoading = rzpLoading || isProcessing || userLoading; + return ( -
- -
+ + + + + + + Complete Your Purchase + + You're about to purchase the {plan.name} plan for {plan.currency}{plan.price} {plan.interval}. + + +
+
+

{plan.name} Plan

+

+ {plan.currency}{plan.price} per {plan.interval} +

+
+ +
+ +
+ {paymentProviders.map((provider) => { + const Icon = provider.icon; + return ( +
setSelectedProvider(provider.id)} + > +
+
+ {selectedProvider === provider.id && ( +
+ )} +
+ +
+
+

{provider.name}

+ {provider.popular && ( + + Popular + + )} +
+

+ {provider.description} +

+
+
+
+ ); + })} +
+
+
+ + + + + +
); }; -export default PaymentComponent; \ No newline at end of file +export default PaymentModal; \ No newline at end of file From 99d40ccd024be1c231c4a4d3b7f9a49fe04430ef Mon Sep 17 00:00:00 2001 From: nishantdotcom Date: Wed, 17 Sep 2025 09:41:44 +0530 Subject: [PATCH 2/2] Otp clear at resend button #230 --- frontend/app/_components/Otp.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/frontend/app/_components/Otp.tsx b/frontend/app/_components/Otp.tsx index 85d19702..364d1fbc 100644 --- a/frontend/app/_components/Otp.tsx +++ b/frontend/app/_components/Otp.tsx @@ -15,6 +15,7 @@ export function Otp({ email }: { email: string }) { const handleResend = async () => { setIsResending(true); + setOtp("") try { const response = await fetch(`${BACKEND_URL}/auth/initiate_signin`, { method: "POST",