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:
- selectedPlan (
PlanData | null) - Current selected plan - availablePlans (
ApiPlan[]) - All available plans from API - currentBilling (
"monthly" | "yearly") - Billing cycle - showPlanSelector (
boolean) - Plan change UI - 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¶
GET /v2/get-user-data/{userId}- Fetch user dataGET /v2/subscriptions/plans- Get all available plansPOST /v2/select-subscription- Select subscription planPOST /v2/subscription-create- Create Razorpay orderPOST /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:
Session Storage¶
authToken- Authentication tokenisLoggedIn- Login statususer_id- User IDsubscriptionActive- Subscription statussubscribedPlan- Selected plan (JSON)
Security Features¶
- Authentication Check: Redirect to login if not authenticated
- Payment ID Validation: Strict validation to prevent undefined/null values
- Signature Verification: Backend verifies Razorpay signature
- HTTPS Required: Razorpay SDK requires secure connection
- 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:
- Select subscription on backend
- Create Razorpay order
- Open Razorpay checkout modal
- User completes payment
- Razorpay callback
- Verify payment on backend
- Activate subscription
- Redirect to dashboard
Test Flow:
- Mock select subscription
- Skip Razorpay
- Mock payment success
- Activate subscription immediately
- 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."