Skip to content

Payment Pages

Purpose: Payment processing and subscription purchase
Category: Commerce
Pages: 3 pages
Payment Gateway: Razorpay integration


Overview

Pages handling payment flows for subscription purchases with comprehensive Razorpay integration.


1. Payment Page

File: src/app/payment/PaymentClient.tsx (980 lines)
Route: /payment
Type: Protected
Purpose: Comprehensive Razorpay payment integration with plan selection and subscription management

⭐ Most Complex Payment Flow - 980 Lines of Production Code


State Variables (14)

Plan Management:

  1. selectedPlan (PlanData | null) - Current selected plan
  2. availablePlans (ApiPlan[]) - All available plans from API
  3. currentBilling ("monthly" | "yearly") - Billing cycle
  4. showPlanSelector (boolean) - Plan change UI
  5. plansLoading (boolean) - Plans fetching state

User Data: 6. userEmail (string) - User email 7. userId (string) - User ID 8. userData ({email, name, user_id} | null) - Full user data

Payment State: 9. razorpayLoaded (boolean) - Razorpay SDK loaded 10. processingPayment (boolean) - Payment in progress 11. orderData (OrderResponse | null) - Created order details 12. isLoading (boolean) - Initial page load


Type Definitions (7 Interfaces)

PlanData:

interface PlanData {
  name: string;
  price: string;
  originalPrice: string | null;
  billingPeriod: string;
  features: string[];
  category: string; // "free" | "starter" | "pro" | "business" | "enterprise"
  billing: "monthly" | "yearly";
}

ApiPlan: (Complete backend plan structure)

interface ApiPlan {
  category: string;
  pricing: { monthly: number | string; yearly?: number | string };
  offers: { monthly: number | string; yearly?: number | string };
  no_of_chatbots: number | null;
  no_of_chat_sessions: { monthly: number | null; yearly?: number | null };
  website_pages_crawl_cap: number | null;
  features: {
    playground: boolean;
    analytics: boolean;
    chat_transcripts: boolean;
    premium_avatars: boolean;
    avatar_selection: boolean;
    voice_selection: boolean;
    ai_data_training_hub: boolean;
    custom_questions: boolean;
    chat_notification: boolean;
    form_submission_notification: boolean;
    form_center: boolean;
    [key: string]: boolean;
  };
}

RazorpayOptions:

interface RazorpayOptions {
  key: string;
  amount: number;
  currency: string;
  name: string;
  description: string;
  order_id?: string;
  handler: (response: RazorpayResponse) => void;
  prefill: { name: string; email: string; contact: string };
  notes: { plan: string; billing: string };
  theme: { color: string };
  modal: { ondismiss: () => void };
}

OrderResponse: (Backend order creation)

interface OrderResponse {
  order_id: string;
  amount: number;
  currency: string;
  user_id: string;
  subscription_plan: string;
  billing_cycle: string;
  razorpay_key_id: string;
  checkout_config: {
    key: string;
    amount: number;
    currency: string;
    name: string;
    description: string;
    order_id: string;
    prefill: { name: string; email: string; contact: string };
    theme: { color: string };
  };
}

Payment Flow (3-Step Process)

Step 1: Select Subscription

const selectSubscription = async () => {
  const formData = new FormData();
  formData.append("user_id", userId);
  formData.append("subscription_plan", selectedPlan.category);
  formData.append("billing_cycle", selectedPlan.billing);

  const response = await fetch("/v2/select-subscription", {
    method: "POST",
    body: formData,
  });

  return response.ok;
};

Step 2: Create Subscription

const createSubscription = async () => {
  const trialDays = selectedPlan.category === "free" ? 90 : 0;

  const formData = new FormData();
  formData.append("user_id", userId);
  formData.append("plan_category", selectedPlan.category);
  formData.append("billing_cycle", selectedPlan.billing);
  formData.append("quantity", "1");
  formData.append("trial_days", trialDays.toString());
  formData.append("customer_email", userData.email);
  formData.append("customer_name", userData.name);
  formData.append("customer_phone", "");

  const response = await fetch("/v2/subscription-create", {
    method: "POST",
    body: formData,
  });

  const subscriptionData: OrderResponse = await response.json();
  return subscriptionData;
};

Step 3: Verify Subscription

const verifySubscription = async (paymentData) => {
  const formData = new FormData();
  formData.append("razorpay_payment_id", paymentData.payment_id);
  formData.append(
    "razorpay_subscription_id",
    paymentData.subscription_id || ""
  );
  formData.append("user_id", userId);
  formData.append("razorpay_signature", paymentData.signature || "");

  const response = await fetch("/v2/verify-subscription", {
    method: "POST",
    body: formData,
  });

  return await response.json();
};

Razorpay Integration

SDK Loading:

<Script
  src="https://checkout.razorpay.com/v1/checkout.js"
  onLoad={handleRazorpayLoad}
  onError={handleRazorpayError}
  strategy="afterInteractive"
/>

Initialize Payment:

const initiatePayment = async () => {
  // Step 1: Select subscription
  const subscriptionSelected = await selectSubscription();
  if (!subscriptionSelected) return;

  // Step 2: Create subscription
  const order = await createSubscription();
  if (!order) return;

  setOrderData(order);

  // Step 3: Open Razorpay checkout
  const options: RazorpayOptions = {
    key: order.checkout_config.key,
    amount: order.checkout_config.amount, // In paise (INR) or cents (USD)
    currency: order.checkout_config.currency,
    name: order.checkout_config.name,
    description: order.checkout_config.description,
    order_id: order.checkout_config.order_id,
    handler: (response: RazorpayResponse) => {
      const subscriptionId =
        response.razorpay_subscription_id || order.order_id;
      handlePaymentSuccess({
        payment_id: response.razorpay_payment_id,
        subscription_id: subscriptionId,
        signature: response.razorpay_signature || "",
        plan: selectedPlan,
      });
    },
    prefill: {
      name: order.checkout_config.prefill.name || "Test User",
      email:
        order.checkout_config.prefill.email || userEmail || "test@example.com",
      contact: order.checkout_config.prefill.contact || "",
    },
    notes: {
      plan: selectedPlan.name,
      billing: selectedPlan.billing,
    },
    theme: {
      color: order.checkout_config.theme.color, // "#FF6622"
    },
    modal: {
      ondismiss: () => setProcessingPayment(false),
    },
  };

  const razorpay = new window.Razorpay(options);
  razorpay.on("payment.failed", handlePaymentFailure);
  razorpay.open();
};

Payment Success Handler

const handlePaymentSuccess = async (paymentData) => {
  setProcessingPayment(true);

  // Validate payment_id
  if (
    !paymentData.payment_id ||
    paymentData.payment_id === "undefined" ||
    paymentData.payment_id === "null"
  ) {
    toast.error("Payment verification failed: Missing payment information");
    return;
  }

  // Verify with backend
  const verificationResult = await verifySubscription({
    subscription_id: paymentData.subscription_id || "",
    payment_id: paymentData.payment_id,
    signature: paymentData.signature || "",
  });

  if (verificationResult) {
    toast.success("Payment successful! Your subscription is now active.");

    // Store subscription data
    sessionStorage.setItem("subscriptionActive", "true");
    sessionStorage.setItem("subscribedPlan", JSON.stringify(selectedPlan));

    // Redirect to dashboard
    setTimeout(() => router.push("/chatbots"), 2000);
  } else {
    // Fallback: Payment success but verification failed
    toast.warning(
      "Payment successful but verification failed. Please contact support."
    );
    sessionStorage.setItem("subscriptionActive", "true");
    setTimeout(() => router.push("/chatbots"), 2000);
  }
};

Plan Management Features

Fetch Available Plans:

const fetchAvailablePlans = async () => {
  const response = await fetch("/v2/subscriptions/plans");
  const data = await response.json();

  // Filter out free plan
  const filteredPlans = data.plans.filter((plan) => plan.category !== "free");
  setAvailablePlans(filteredPlans);
};

Change Plan:

const changePlan = (category: string) => {
  const apiPlan = availablePlans.find((plan) => plan.category === category);
  if (apiPlan) {
    const newPlan = convertApiPlanToPlanData(apiPlan, currentBilling);
    setSelectedPlan(newPlan);
    setShowPlanSelector(false);
    toast.success(`Plan changed to ${newPlan.name}`);
  }
};

Toggle Billing:

const toggleBilling = () => {
  const newBilling = currentBilling === "monthly" ? "yearly" : "monthly";
  setCurrentBilling(newBilling);

  if (selectedPlan) {
    const apiPlan = availablePlans.find(
      (plan) => plan.category === selectedPlan.category
    );
    if (apiPlan) {
      const updatedPlan = convertApiPlanToPlanData(apiPlan, newBilling);
      setSelectedPlan(updatedPlan);
      toast.success(`Billing changed to ${newBilling}`);
    }
  }
};

API Endpoints

  1. GET /v2/get-user-data/{userId} - Fetch user data
  2. GET /v2/subscriptions/plans - Get all available plans
  3. POST /v2/select-subscription - Select subscription plan
  4. POST /v2/subscription-create - Create Razorpay order
  5. POST /v2/verify-subscription - Verify payment

URL Parameters

Plan Data from Pricing Page:

const planData = searchParams.get("plan");
const plan = JSON.parse(decodeURIComponent(planData));
setSelectedPlan(plan);
setCurrentBilling(plan.billing || "monthly");

Error Handling

Validation Errors:

  • Missing payment_id
  • Invalid payment data
  • Missing user authentication

API Errors:

  • Plan mapping not found → "Payment system is being configured"
  • Subscription creation failed
  • Verification failed

Razorpay Errors:

const handlePaymentFailure = (error: RazorpayError) => {
  console.error("Payment failed:", error);
  toast.error("Payment failed. Please try again.");
};

UI Components

Header:

  • "Complete Your Subscription" title
  • Back to pricing button
  • Current plan badge

Plan Summary Card:

  • Plan name and billing period
  • Price display (with original price strikethrough if discount)
  • Features list with checkmarks
  • "Features included:" section

Plan Change Section:

  • Billing toggle (Monthly ↔ Yearly)
  • Plan selector dropdown
  • Available plans filtered (no free)
  • Dynamic price updates

Payment Button:

<button
  onClick={initiatePayment}
  disabled={processingPayment || !razorpayLoaded}
  className="w-full bg-orange-500 text-white py-3 rounded font-semibold"
>
  {processingPayment ? "Processing..." : "Proceed to Payment"}
</button>

Loading States

Initial Load:

<div className="animate-spin rounded-full h-32 w-32 border-b-2 border-orange-500"></div>
<p>Loading payment page...</p>

Processing Payment:

<button disabled>Processing...</button>

Session Storage

  • authToken - Authentication token
  • isLoggedIn - Login status
  • user_id - User ID
  • subscriptionActive - Subscription status
  • subscribedPlan - Selected plan (JSON)

Security Features

  1. Authentication Check: Redirect to login if not authenticated
  2. Payment ID Validation: Strict validation to prevent undefined/null values
  3. Signature Verification: Backend verifies Razorpay signature
  4. HTTPS Required: Razorpay SDK requires secure connection
  5. User ID Validation: Required for all API calls

Responsive Design

Desktop:

  • Two-column layout (summary + form)
  • Side-by-side plan details
  • Full-width payment button

Mobile:

  • Single column stack
  • Compact plan cards
  • Touch-friendly buttons

Toast Notifications

Success:

  • "Plan changed to {name}"
  • "Billing changed to {cycle}"
  • "Payment successful! Your subscription is now active."

Error:

  • "Please login to continue"
  • "No plan selected"
  • "Failed to load pricing data"
  • "Payment failed. Please try again."

Warning:

  • "Payment successful but verification failed. Please contact support."

2. Test Payment

File: src/app/test-payment/page.tsx
Route: /test-payment
Type: Protected (Dev/QA only)
Purpose: Test payment flow without Razorpay

Features:

  • Mock payment success/failure
  • Skip Razorpay integration
  • Direct subscription activation
  • Useful for development/testing

Note: Should be disabled/removed in production


3. Test Payment Simple

File: src/app/test-payment-simple/page.tsx
Route: /test-payment-simple
Type: Protected (Dev/QA only)
Purpose: Simplified test payment flow

Features:

  • Minimal UI
  • One-click payment simulation
  • Instant subscription activation
  • No form validation

Note: Development/testing only


Summary

Page Lines Environment Complexity
PaymentClient 980 Production ⭐⭐⭐⭐⭐
Test Payment ~300 Dev/QA ⭐⭐
Test Payment Simple ~200 Dev/QA

Total: ~1,480 lines of payment code


Production vs Test

Production Flow:

  1. Select subscription on backend
  2. Create Razorpay order
  3. Open Razorpay checkout modal
  4. User completes payment
  5. Razorpay callback
  6. Verify payment on backend
  7. Activate subscription
  8. Redirect to dashboard

Test Flow:

  1. Mock select subscription
  2. Skip Razorpay
  3. Mock payment success
  4. Activate subscription immediately
  5. Redirect to dashboard

Environment Variables:

NEXT_PUBLIC_API_BASE_URL=https://api.machineavatars.com
# Razorpay key ID provided by backend via checkout_config

"From plan selection to payment verification, every step secured."