From 111948b283c685283bcb1dff63012556f06d14aa Mon Sep 17 00:00:00 2001 From: Arghya721 Date: Sun, 11 Aug 2024 01:02:29 +0530 Subject: [PATCH] refactor: Implement lazy loading for images in DashboardV2 and update chat route --- app.py | 130 ++++++++++- web/.env.example | 3 +- web/src/App.js | 23 +- web/src/components/PlansModal.js | 356 +++++++++++++++---------------- 4 files changed, 325 insertions(+), 187 deletions(-) diff --git a/app.py b/app.py index ca33083..03803d2 100644 --- a/app.py +++ b/app.py @@ -39,9 +39,12 @@ from razorpay.resources.subscription import Subscription from razorpay.resources.customer import Customer from razorpay.resources.plan import Plan +from razorpay.resources.order import Order import tiktoken from anthropic import Anthropic from vertexai.preview import tokenization +import hmac +import hashlib @@ -130,8 +133,6 @@ class ChatUserHistory(BaseModel): chat_model: str created_at: datetime.datetime updated_at: datetime.datetime - - class ChatByIdHistory(BaseModel): """Chat by id history model for the chat by id endpoint.""" ai_message: str @@ -141,6 +142,12 @@ class ChatByIdHistory(BaseModel): regenerate_message: bool model: str +class PaymentRequest(BaseModel): + """Payment request model for the payment endpoint.""" + razorpay_order_id: str + razorpay_payment_id: str + razorpay_signature: str + class SubscriptionRequest(BaseModel): """Subscription request model for the subscription endpoint.""" redirect_url: str @@ -1016,9 +1023,128 @@ async def is_user_subscribed(token_info: dict = Depends(verify_token)): raise HTTPException(status_code=500, detail="Internal server error") from e +@app.post("/v1/create_order", tags=["Order Endpoints"]) +async def create_order(plan_id: str, token_info: dict = Depends(verify_token)): + """Create an order for the user.""" + try: + amount = 0 + if plan_id == 'plan_50': + amount = 420 + elif plan_id == 'plan_250': + amount = 840 + elif plan_id == 'plan_500': + amount = 1680 + else: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail="Invalid plan id", + ) + + order_data = { + "amount" : amount*100, + "currency" : "INR", + "receipt": plan_id, + } + + + order = Order(client).create(order_data) + # save the order into the orders collection + db.collection('orders').add({ + 'order_id': order['id'], + 'plan_id': plan_id, + 'customer_id': token_info['sub'], + 'created_at': google_firestore.SERVER_TIMESTAMP, + 'updated_at': google_firestore.SERVER_TIMESTAMP, + }) + + response = { + "order_id": order['id'], + "amount": amount, + "currency": "INR", + "receipt": plan_id, + } + + # only return the order id + return response + except HTTPException as he: + raise he + except Exception as e: + logging.error("Error creating order: %s", e) + raise HTTPException(status_code=500, detail="Internal server error") from e + + +@app.post("/v1/verify_payment", tags=["Order Endpoints"]) +async def verify_payment(request: PaymentRequest, token_info: dict = Depends(verify_token)): + """Verify the payment for the user.""" + try: + # first check if the payment_id exists in the payments collection + payment_ref = db.collection('payments').where('payment_id', '==', request.razorpay_payment_id).stream() + payment_data = next(payment_ref, None) + + if payment_data: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail="Payment already exists", + ) + + # verify the razorpay_signature + generated_signature = hmac.new( + RAZORPAY_KEY_SECRET.encode('utf-8'), + f"{request.razorpay_order_id}|{request.razorpay_payment_id}".encode('utf-8'), + hashlib.sha256 + ).hexdigest() + + if generated_signature != request.razorpay_signature: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail="Invalid signature", + ) + + order_data = Order(client).fetch(request.razorpay_order_id) + # check if the order is paid + if order_data['status'] == 'paid': + + db.collection('payments').add({ + 'order_id': request.razorpay_order_id, + 'payment_id': request.razorpay_payment_id, + 'customer_id': token_info['sub'], + 'created_at': google_firestore.SERVER_TIMESTAMP, + 'updated_at': google_firestore.SERVER_TIMESTAMP, + }) + + print(order_data) + # get the current remaining generations for the user + generations_left = get_generations(token_info) + # update user_generations collection by adding the remaining generations + if order_data['receipt'] == 'plan_50': + db.collection('user_generations').document(token_info['sub']).update({ + 'remaining_generations': 50 + generations_left, + 'updated_at': google_firestore.SERVER_TIMESTAMP, + }) + elif order_data['receipt'] == 'plan_250': + db.collection('user_generations').document(token_info['sub']).update({ + 'remaining_generations': 250 + generations_left, + 'updated_at': google_firestore.SERVER_TIMESTAMP, + }) + elif order_data['receipt'] == 'plan_500': + db.collection('user_generations').document(token_info['sub']).update({ + 'remaining_generations': 500 + generations_left, + 'updated_at': google_firestore.SERVER_TIMESTAMP, + }) + return {"status": "success"} + else: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail="Payment failed", + ) + except HTTPException as he: + raise he + except Exception as e: + logging.error("Error verifying payment: %s", e) + raise HTTPException(status_code=500, detail="Internal server error") from e if __name__ == "__main__": diff --git a/web/.env.example b/web/.env.example index b358630..dcabe0f 100644 --- a/web/.env.example +++ b/web/.env.example @@ -1,2 +1,3 @@ REACT_APP_GOOGLE_CLIENT_ID= -REACT_APP_API_HOST=http://localhost:5000 \ No newline at end of file +REACT_APP_API_HOST=http://localhost:5000 +RAZOR_PAY_KEY_ID= \ No newline at end of file diff --git a/web/src/App.js b/web/src/App.js index 6c48bdb..81d0ed4 100644 --- a/web/src/App.js +++ b/web/src/App.js @@ -1,11 +1,32 @@ import { NextUIProvider } from "@nextui-org/react"; -import { Dashboard } from "./pages/Dashboard"; +import React, { useEffect } from "react"; import { LoginPage } from "./pages/LoginPage"; import { BrowserRouter as Router, Routes, Route } from "react-router-dom"; import { ProtectedPage } from "./pages/ProtectedPage"; import { DashboardV2 } from "./pages/DashboardV2"; + + function App() { + const loadScript = (src) => { + return new Promise((resolve) => { + const script = document.createElement("script"); + script.src = src; + script.onload = () => { + resolve(true); + }; + script.onerror = () => { + resolve(false); + }; + document.body.appendChild(script); + }); + }; + + useEffect(() => { + loadScript("https://checkout.razorpay.com/v1/checkout.js"); + }); + + return ( diff --git a/web/src/components/PlansModal.js b/web/src/components/PlansModal.js index 1585908..912790e 100644 --- a/web/src/components/PlansModal.js +++ b/web/src/components/PlansModal.js @@ -1,205 +1,195 @@ -import React, { useEffect, useState } from "react"; -import { Modal, ModalContent, ModalHeader, ModalBody, ModalFooter, Button, Spinner } from "@nextui-org/react"; -import axios from "axios"; -import congratulations from "../images/congratulations.gif"; +import React, { useState } from "react"; +import { Modal, ModalContent, ModalHeader, ModalBody, ModalFooter, Button, Card, CardHeader, CardBody, CardFooter } from "@nextui-org/react"; +import { themes } from "prism-react-renderer"; export const PlansModal = ({ isOpen, onClose }) => { const API_HOST = process.env.REACT_APP_API_HOST || "http://localhost:5000"; const accessToken = localStorage.getItem("accessToken"); - const [plans, setPlans] = useState(); - const [loading, setLoading] = useState(false); - const [paymentStatus, setPaymentStatus] = useState(null); + const plans = [ + { + name: 'Basic', + description: '', + price: 420, + planId: "plan_50", + features: [ + '50 Generations on all models', + ], + }, + { + name: 'Standard', + description: '', + price: 840, + planId: "plan_250", + features: [ + '250 Generations on all models', + ], + }, + { + name: 'Premium', + description: '', + price: 1680, + planId: "plan_500", + features: [ + '500 Generations on all models', + ], + }, + ]; - const createSubscription = async () => { - setLoading(true); + const [selectedPlan, setSelectedPlan] = useState(null); + const [isPaymentOpen, setIsPaymentOpen] = useState(false); // State to handle Razorpay modal + + const getOrderId = async (planId) => { try { - const redirectUrl = window.location.href; + const response = await fetch(`${API_HOST}/v1/create_order?plan_id=${planId}`, { + method: "POST", + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${accessToken}`, + }, + }); + const data = await response.json(); + onClose(); // Close the parent modal - const response = await axios.post( - `${API_HOST}/v1/subscriptions`, - { - "redirect_url": redirectUrl, + console.log(data); + + // Configure Razorpay options + const options = { + key: process.env.REACT_APP_RAZOR_PAY_KEY_ID, + amount: data.amount, + currency: data.currency, + name: "Chat With LLMs", + description: plans.find((plan) => plan.planId === planId).name, + order_id: data.order_id, + handler: function (response) { + verfiyPayment(response); // Verify payment + setIsPaymentOpen(false); // Close Razorpay modal + alert("Payment successful"); + // make the page reload to update the user's plan + window.location.reload(); + }, + prefill: { + name: "John Doe", + email: "johnDoe@xxx.com", + contact: "9999999999", }, - { - headers: { - Authorization: `Bearer ${accessToken}`, - }, + theme : { + color: "#3399cc", + }, + modal: { + ondismiss: function () { + setIsPaymentOpen(false); // Handle closing of Razorpay modal + } } - ); - - if (response.data && response.data.short_url) { - const paymentPopup = window.open( - response.data.short_url, - 'RazorpayPayment', - 'width=600,height=600,resizable=yes,scrollbars=yes,status=yes' - ); + }; - if (!paymentPopup || paymentPopup.closed || typeof paymentPopup.closed == 'undefined') { - alert("Popup blocked. Please allow popups for this site to proceed with the payment."); - setLoading(false); - } else { - // Check if the popup is closed every second - const checkPopupClosed = setInterval(() => { - if (paymentPopup.closed) { - clearInterval(checkPopupClosed); - handlePopupClosed(); - } - }, 1000); - } - } else { - console.error('No payment URL received'); - setLoading(false); - } + // Open Razorpay modal + setIsPaymentOpen(true); + const rzp = new window.Razorpay(options); + rzp.open(); + } catch (error) { - console.error('Error creating subscription:', error); - setLoading(false); + console.error("Error:", error); } }; - const checkSubscriptionStatus = async () => { + const verfiyPayment = async (paymentRequest) => { try { - // fetch the latest subscription - const response = await axios.get( - `${API_HOST}/v1/fetch_subscription`, - { - headers: { - Authorization: `Bearer ${accessToken}`, - }, - } - ); - // if response is 200, then get the top level key of the response - if (response.status === 200 && response.data.length > 0) { - const subscriptionId = response.data[0].subscription_id; - // check the subscription status - const subscriptionStatus = await axios.get( - `${API_HOST}/v1/subscriptions/${subscriptionId}/status`, - { - headers: { - Authorization: `Bearer ${accessToken}`, - }, - } - ); - console.log('Subscription status:', subscriptionStatus.data); - // if the subscription status is active, then set the payment status to success - if (subscriptionStatus?.data?.status === 'active') { - return true; - } else { - return false; - } - } - } - catch (error) { - console.error('Error fetching subscription:', error); - } - } - - const handlePopupClosed = async () => { - try { - const subscriptionStatus = await checkSubscriptionStatus(); - if (subscriptionStatus) { - setPaymentStatus('success'); - } else { - setPaymentStatus('failed'); - } - setLoading(false); + const response = await fetch(`${API_HOST}/v1/verify_payment`, { + method: "POST", + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${accessToken}`, + }, + body: JSON.stringify(paymentRequest), + }); + const data = await response.json(); + console.log(data); } catch (error) { - console.error('Error handling popup closed:', error); - setLoading(false); - } finally { - setLoading(false); + console.error("Error:", error); } - }; - - useEffect(() => { - - const fetchPlans = async () => { - try { - const response = await axios.get(`${API_HOST}/v1/plans`, { - headers: { - Authorization: `Bearer ${accessToken}`, - }, - }); - setPlans(response.data); - } catch (error) { - console.error('Error fetching plans:', error); - } - }; - - checkSubscriptionStatus().then((status) => { - if (status) { - setPaymentStatus('success'); - } else { - fetchPlans(); - } - }); - - }, [API_HOST, accessToken]); + } return ( - - - -

Plus Subscription

-
- - {paymentStatus === 'success' ? ( -
- Congratulations -

- Congratulations! You can now enjoy the Plus subscription and its benefits. -

-

- Explore advanced features and powerful AI models at your fingertips. -

-
- ) : ( -
-
- Price: - ₹ {plans?.amount} -
-
- - - - Early access to new features -
-
- - - - Access to powerful models like GPT-4, Claude-Opus, Gemini-1.5-Pro, and many more -
-
- )} -
- - {paymentStatus === 'success' ? ( - - ) : ( - + <> + + + {(onClose) => ( + <> + +

Choose Your Plan

+

Select a plan that suits your needs

+
+ +
+ {plans.map((plan, index) => ( + setSelectedPlan(plan.name)} + > + +

{plan.name}

+

{plan.description}

+
+ +
+ ₹{plan.price} + /- +
+
    + {plan.features.map((feature, i) => ( +
  • + + + + {feature} +
  • + ))} +
+
+ + + +
+ ))} +
+
+ + + + )} -
-
-
+ + + ); -}; \ No newline at end of file +};