SentinelScan-WSS / frontend /src /components /PricingSection.jsx
grateful790's picture
Add Razorpay checkout with platform fee and INR/USD pricing
335a9ba
Raw History Blame Contribute Delete
24.2 kB
/* eslint-disable react-hooks/exhaustive-deps, react-hooks/set-state-in-effect, react-hooks/immutability */
import React, { useState, useEffect } from 'react';
import { useAuth } from './AuthContext';
import { useNavigate } from 'react-router-dom';
import {
Shield,
Zap,
Check,
Target,
ArrowRight,
Briefcase
} from 'lucide-react';
const RAZORPAY_SCRIPT = 'https://checkout.razorpay.com/v1/checkout.js';
const FALLBACK_TIERS = [
{ id: 'quick', name: 'Quick Scan', monthly_price: 499, monthly_price_inr: 49900 },
{ id: 'advanced', name: 'Advanced Scan', monthly_price: 4499, monthly_price_inr: 399900 },
{ id: 'deep', name: 'Deep Scan', monthly_price: 9999, monthly_price_inr: 899900 },
{ id: 'enterprise', name: 'Custom Solutions', monthly_price: 0, monthly_price_inr: 0 }
];
let razorpayScriptPromise = null;
const loadRazorpayScript = () => {
if (typeof window === 'undefined') return Promise.reject(new Error('Browser only'));
if (window.Razorpay) return Promise.resolve(true);
if (razorpayScriptPromise) return razorpayScriptPromise;
razorpayScriptPromise = new Promise((resolve, reject) => {
const script = document.createElement('script');
script.src = RAZORPAY_SCRIPT;
script.async = true;
script.onload = () => resolve(true);
script.onerror = () => {
razorpayScriptPromise = null;
reject(new Error('Unable to load the Razorpay checkout. Check your network or ad-blocker.'));
};
document.body.appendChild(script);
});
return razorpayScriptPromise;
};
const detectCurrency = () => {
try {
const timeZone = Intl.DateTimeFormat().resolvedOptions().timeZone || '';
const locale = new Intl.Locale(navigator.language || 'en-US');
if (timeZone === 'Asia/Kolkata' || locale.region === 'IN') return 'INR';
} catch {
// Intl.Locale unsupported - fall through to USD
}
return 'USD';
};
const formatMoney = (minor, currency) => {
const value = Number(minor || 0) / 100;
if (currency === 'INR') {
return `₹${value.toLocaleString('en-IN', {
minimumFractionDigits: value % 1 === 0 ? 0 : 2,
maximumFractionDigits: 2
})}`;
}
return `$${value.toFixed(2)}`;
};
export default function PricingSection({ embedded = false, hideCurrentPlan = false }) {
const { user, token, refreshAccessToken } = useAuth();
const navigate = useNavigate();
const [loadingPlan, setLoadingPlan] = useState(null);
const [paymentSuccess, setPaymentSuccess] = useState(false);
const [paymentError, setPaymentError] = useState(null);
const [dynamicPrices, setDynamicPrices] = useState({});
const [currency, setCurrency] = useState(detectCurrency);
const [quote, setQuote] = useState(null);
const [orderLoading, setOrderLoading] = useState(false);
const [feeConfig, setFeeConfig] = useState({ platform_fee_percent: 2, gst_percent: 18 });
useEffect(() => {
fetch('/api/billing/tiers')
.then(res => res.json())
.then(data => {
const prices = {};
data.forEach(t => {
prices[t.id] = {
monthly_price: t.monthly_price,
monthly_price_inr: t.monthly_price_inr
};
});
setDynamicPrices(prices);
})
.catch(err => console.error('Failed to load dynamic prices', err));
fetch('/api/billing/config')
.then(res => (res.ok ? res.json() : null))
.then(data => {
if (data && data.platform_fee_percent !== undefined) {
setFeeConfig({
platform_fee_percent: data.platform_fee_percent,
gst_percent: data.gst_percent || 0
});
}
})
.catch(() => {});
}, []);
const authFetch = async (url, options = {}) => {
let res = await fetch(url, options);
if (res.status === 401 && refreshAccessToken) {
const newToken = await refreshAccessToken();
if (newToken) {
res = await fetch(url, {
...options,
headers: {
...(options.headers || {}),
'Authorization': `Bearer ${newToken}`
}
});
}
}
return res;
};
const priceFor = (planId) => {
if (planId === 'enterprise') return 'Custom';
const tier = dynamicPrices[planId] || FALLBACK_TIERS.find(t => t.id === planId);
if (!tier) return '—';
const minor = currency === 'INR' ? tier.monthly_price_inr : tier.monthly_price;
return formatMoney(minor, currency);
};
const handleSubscribe = async (planId) => {
if (planId === 'enterprise') {
window.location.href = '#booking';
return;
}
if (!user) {
navigate('/login');
return;
}
setLoadingPlan(planId);
try {
const res = await authFetch('/api/billing/quote', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ tier_id: planId, currency })
});
const data = await res.json();
if (!res.ok) {
setPaymentError(data.message || 'Could not calculate the order total.');
setLoadingPlan(null);
return;
}
setQuote(data);
} catch (err) {
console.error(err);
setPaymentError('Could not reach the payment service. Please try again.');
}
setLoadingPlan(null);
};
const verifyRazorpayPayment = async (response) => {
try {
const res = await authFetch('/api/billing/verify-payment', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`
},
body: JSON.stringify({
razorpay_order_id: response.razorpay_order_id,
razorpay_payment_id: response.razorpay_payment_id,
razorpay_signature: response.razorpay_signature
})
});
const data = await res.json().catch(() => ({}));
if (res.ok && data.status === 'success') {
setPaymentSuccess(true);
setQuote(null);
setLoadingPlan(null);
setTimeout(() => {
window.location.reload();
}, 3500);
return;
}
setPaymentError(data.message || 'We could not verify the payment. If money was deducted, contact support with your payment id.');
} catch (err) {
console.error(err);
setPaymentError('Payment verification failed due to a network error. Please contact support before retrying.');
}
setLoadingPlan(null);
};
const confirmPayment = async () => {
if (!quote) return;
setOrderLoading(true);
try {
const res = await authFetch('/api/billing/create-order', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`
},
body: JSON.stringify({ tier_id: quote.tier_id, currency: quote.currency })
});
const order = await res.json().catch(() => ({}));
if (!res.ok || !order.order_id) {
setPaymentError(order.message || 'Could not start the payment. Please try again.');
setOrderLoading(false);
return;
}
await loadRazorpayScript();
const options = {
key: order.key_id,
amount: order.amount,
currency: order.currency,
name: 'LarShield',
description: order.description,
order_id: order.order_id,
prefill: order.prefill,
notes: { tier_id: order.tier.id },
theme: { color: '#2563eb' },
modal: {
ondismiss: () => {
setLoadingPlan(null);
setOrderLoading(false);
}
},
handler: (response) => {
verifyRazorpayPayment(response);
}
};
const checkout = new window.Razorpay(options);
checkout.on('payment.failed', (resp) => {
const description = resp?.error?.description || 'Payment failed. Please try a different method.';
setPaymentError(description);
setLoadingPlan(null);
});
setQuote(null);
setOrderLoading(false);
checkout.open();
} catch (err) {
console.error(err);
setPaymentError(err.message || 'Could not open the Razorpay checkout.');
setOrderLoading(false);
setLoadingPlan(null);
}
};
const plans = [
{
id: 'quick',
name: 'Quick Scan',
icon: <Zap className="w-8 h-8 text-primary" />,
description: 'Essential reconnaissance & basic security health checks.',
scanners: '13 Security Modules',
features: [
'3 Scans for 1 Target Website',
'Fast Recon (Headers, Nmap Quick, WHOIS, WAF)',
'DNS Security & SSL/TLS Audit',
'Cookies, CSP & Clickjacking Checks',
'Git Repository Exposure Detection',
'Instant PDF Report Export'
],
popular: false
},
{
id: 'advanced',
name: 'Advanced Scan',
icon: <Shield className="w-8 h-8 text-primary" />,
description: 'Core vulnerability auditing & deep path analysis.',
scanners: '36 Security Modules',
popular: false,
features: [
'3 Scans for 1 Target Website',
'Everything in Quick Scan',
'Subdomain Discovery (Subfinder, Amass, crt.sh)',
'XSS, SQL Injection & Path Traversal Fuzzing',
'REST API, Cloud Bucket & Secrets Leak Scan',
'CVE Database & Nikto Web Server Engine',
'AI Remediation & Strategy Generator'
]
},
{
id: 'deep',
name: 'Deep Scan',
icon: <Target className="w-8 h-8 text-primary" />,
description: 'Exhaustive threat inspection & active DAST attack simulation.',
scanners: '89 Security Modules',
popular: true,
features: [
'3 Scans for 1 Target Website',
'Everything in Advanced Scan',
'Full 65535-Port Nmap with NSE Vulnerability Scripts',
'Nuclei Scanner Engine (Critical/High/Med/Low)',
'OWASP ZAP DAST Engine (Active Attack Mode)',
'XXE, SSTI, IDOR, GraphQL & Business Logic Flaws',
'HTTP/2 Desync, JS Supply Chain & SAML/OAuth Bypasses'
]
},
{
id: 'enterprise',
name: 'Custom Solutions',
icon: <Briefcase className="w-8 h-8 text-primary" />,
price: 'Custom',
description: 'Tailored VAPT services and dedicated security infrastructure.',
scanners: 'Expert Manual VAPT',
popular: false,
features: [
'Everything in Deep Scan',
'Quarterly Manual VAPT',
'Dedicated Security Architect',
'Custom Compliance Reporting',
'On-Premise Deployment Options',
'1-Hour SLA'
]
}
];
const gstApplies = currency === 'INR';
const feeNote = gstApplies
? `+ ${feeConfig.platform_fee_percent}% platform fee + ${feeConfig.gst_percent}% GST on the fee`
: `+ ${feeConfig.platform_fee_percent}% platform fee at checkout`;
return (
<div className={`${embedded ? 'py-8' : 'py-12'} bg-surface-container-lowest text-on-surface px-4 sm:px-6 relative overflow-hidden border-b border-outline-variant/30`}>
{paymentError && (
<div className="fixed bottom-10 left-1/2 -translate-x-1/2 z-[100] flex items-center bg-error text-on-error px-md py-sm rounded-lg shadow-xl animate-fade-in gap-sm border border-on-error/20 max-w-[90vw]">
<span className="material-symbols-outlined">error</span>
<span className="font-bold text-[14px]">{paymentError}</span>
<button onClick={() => setPaymentError(null)} className="ml-md text-on-error/80 hover:text-on-error bg-transparent border-0 cursor-pointer p-0 flex items-center">
<span className="material-symbols-outlined text-[18px]">close</span>
</button>
</div>
)}
{paymentSuccess && (
<div className="fixed inset-0 z-[100] flex items-center justify-center bg-surface-container-lowest/80 backdrop-blur-sm animate-fade-in">
<div className="bg-surface-container border border-outline-variant rounded-2xl p-xl shadow-2xl max-w-sm w-full text-center flex flex-col items-center transform animate-scale-up">
<div className="w-20 h-20 bg-emerald-500/20 text-emerald-500 rounded-full flex items-center justify-center mb-md shadow-sm border border-emerald-500/30">
<span className="material-symbols-outlined text-[48px] animate-pulse">check_circle</span>
</div>
<h2 className="font-headline-md font-bold text-on-surface mb-sm">Payment Successful</h2>
<p className="font-body-md text-on-surface-variant mb-lg">Your Razorpay payment is verified. We are unlocking your scan package...</p>
<div className="w-full bg-surface-container-highest rounded-full h-1.5 overflow-hidden">
<div className="bg-emerald-500 h-full animate-[progress_3.5s_ease-in-out_forwards]"></div>
</div>
</div>
</div>
)}
{quote && (
<div className="fixed inset-0 z-[100] flex items-center justify-center bg-surface-container-lowest/80 backdrop-blur-sm px-4">
<div className="bg-surface-container border border-outline-variant rounded-2xl p-lg shadow-2xl max-w-md w-full">
<div className="flex items-start justify-between mb-md">
<div>
<h2 className="font-headline-md font-bold text-on-surface">Order Summary</h2>
<p className="font-body-sm text-on-surface-variant text-[12.5px] mt-1">{quote.tier_name}</p>
</div>
<button
onClick={() => setQuote(null)}
className="bg-transparent border-0 cursor-pointer text-on-surface-variant hover:text-on-surface p-1"
aria-label="Close"
>
<span className="material-symbols-outlined text-[20px]">close</span>
</button>
</div>
<div className="bg-surface-container-lowest border border-outline-variant/60 rounded-xl p-sm text-[13px]">
<div className="flex justify-between py-1.5 border-b border-outline-variant/30">
<span className="text-on-surface-variant font-medium">Service price</span>
<span className="font-bold text-on-surface">
{formatMoney(quote.base_amount, quote.currency)}
</span>
</div>
<div className="flex justify-between py-1.5 border-b border-outline-variant/30">
<span className="text-on-surface-variant font-medium">
Platform fee ({quote.fee_percent}%)
</span>
<span className="font-bold text-on-surface">
{formatMoney(quote.platform_fee, quote.currency)}
</span>
</div>
{quote.tax_amount > 0 && (
<div className="flex justify-between py-1.5 border-b border-outline-variant/30">
<span className="text-on-surface-variant font-medium">
GST on platform fee ({quote.gst_percent}%)
</span>
<span className="font-bold text-on-surface">
{formatMoney(quote.tax_amount, quote.currency)}
</span>
</div>
)}
<div className="flex justify-between pt-2 mt-1 border-t-2 border-outline-variant/60">
<span className="font-extrabold text-on-surface">Total payable</span>
<span className="font-extrabold text-primary text-[16px]">
{formatMoney(quote.total_amount, quote.currency)}
</span>
</div>
</div>
<div className="flex items-center gap-xs mt-md bg-primary/10 border border-primary/20 rounded-lg px-3 py-2">
<span className="material-symbols-outlined text-primary text-[18px]">verified</span>
<span className="text-[12px] font-semibold text-on-surface">
Includes {quote.scans_included} scans for 1 target website
</span>
</div>
<p className="text-[11.5px] text-on-surface-variant mt-sm leading-relaxed">
Secure checkout by Razorpay. UPI, credit/debit cards, netbanking, wallets and international cards accepted.
</p>
<div className="flex gap-sm mt-md">
<button
onClick={() => setQuote(null)}
disabled={orderLoading}
className="flex-1 py-2.5 rounded-lg font-bold text-sm border border-outline-variant bg-surface-container-lowest text-on-surface cursor-pointer disabled:opacity-60"
>
Cancel
</button>
<button
onClick={confirmPayment}
disabled={orderLoading}
className="flex-1 py-2.5 rounded-lg font-bold text-sm border-0 bg-primary text-on-primary shadow-md cursor-pointer hover:opacity-90 disabled:opacity-60 flex items-center justify-center gap-xs"
>
{orderLoading ? (
<>
<span className="material-symbols-outlined animate-spin text-[18px]">sync</span>
Opening...
</>
) : (
<>Pay {formatMoney(quote.total_amount, quote.currency)}</>
)}
</button>
</div>
</div>
</div>
)}
{/* Background Soft Gradients (Adapted to Light Theme) */}
<div className="absolute top-0 left-1/4 w-[500px] h-[500px] bg-primary/5 rounded-full blur-[100px] -z-10 pointer-events-none"></div>
<div className="absolute bottom-0 right-1/4 w-[600px] h-[600px] bg-secondary/5 rounded-full blur-[100px] -z-10 pointer-events-none"></div>
<div className="max-w-[1400px] mx-auto">
<div className="text-center mb-10 relative">
<div className="inline-flex items-center space-x-2 bg-surface-container-low border border-outline-variant/60 rounded-full px-4 py-1 mb-4 shadow-sm">
<Shield className="w-3.5 h-3.5 text-primary" />
<span className="font-label-sm text-on-surface font-bold tracking-wider uppercase text-[10px]">LarShield Target Scan Packages</span>
</div>
<h1 className="font-display-lg text-3xl md:text-4xl font-extrabold text-on-surface mb-4 tracking-tight">
Single Domain Scan Packages
</h1>
<p className="font-body-md text-base text-on-surface-variant max-w-2xl mx-auto mb-4 leading-relaxed">
Purchase a plan once and perform up to <strong className="text-on-surface font-extrabold">3 complete scans</strong> on the same target website.
</p>
<div className="inline-flex items-center gap-xs bg-primary/10 text-primary border border-primary/20 rounded-full px-4 py-1.5 text-xs font-bold shadow-sm mb-4">
<span className="material-symbols-outlined text-[16px]">verified</span>
1 Plan Purchase = 3 Scans Allowed for 1 Target Website
</div>
<div className="flex items-center justify-center gap-sm">
<div className="inline-flex items-center bg-surface-container-low border border-outline-variant rounded-full p-1">
{['INR', 'USD'].map((code) => (
<button
key={code}
onClick={() => { setCurrency(code); setQuote(null); }}
className={`px-3 py-1 rounded-full text-[12px] font-bold border-0 cursor-pointer transition-all ${currency === code
? 'bg-primary text-on-primary shadow-sm'
: 'bg-transparent text-on-surface-variant hover:text-on-surface'
}`}
>
{code === 'INR' ? '₹ INR' : '$ USD'}
</button>
))}
</div>
<span className="text-[11.5px] text-on-surface-variant font-medium">
{currency === 'INR' ? 'India (UPI, cards, netbanking)' : 'International cards'}
</span>
</div>
</div>
{/* Pricing Cards */}
<div className="grid md:grid-cols-2 lg:grid-cols-4 gap-6 w-full mx-auto z-10 relative">
{plans.map((plan) => {
const isCurrentPlan = hideCurrentPlan ? false : user?.subscription_tier === plan.id;
return (
<div
key={plan.id}
className={`relative bg-surface-container-lowest rounded-2xl p-6 transition-all duration-300 flex flex-col group border-2 ${plan.popular ? 'border-primary shadow-xl scale-[1.02]' : 'border-outline-variant/50 shadow-sm hover:border-primary/50 hover:-translate-y-1 hover:shadow-lg'}`}
>
{plan.popular && (
<div className="absolute -top-3.5 left-1/2 -translate-x-1/2 bg-primary text-on-primary px-4 py-1 rounded-full font-label-sm text-[10px] font-extrabold uppercase tracking-wider shadow-sm whitespace-nowrap">
Recommended
</div>
)}
<div className="mb-4 flex flex-col gap-xs">
<div className="w-10 h-10 rounded-xl flex items-center justify-center mb-1 transition-transform duration-300 bg-primary/10">
{React.cloneElement(plan.icon, { className: 'w-5 h-5 text-primary' })}
</div>
<h3 className="font-headline-md text-lg font-extrabold text-on-surface">{plan.name}</h3>
<p className="font-body-sm text-on-surface-variant text-[12.5px] leading-snug min-h-[36px]">{plan.description}</p>
</div>
<div className="mb-2 flex items-baseline">
<span className="font-display-lg text-3xl lg:text-4xl font-extrabold text-on-surface tracking-tight">
{priceFor(plan.id)}
</span>
</div>
{plan.id !== 'enterprise' && (
<p className="text-[10.5px] text-on-surface-variant font-semibold mb-3">{feeNote}</p>
)}
<div className="mb-4 border-t border-outline-variant/40 pt-3">
<div className="inline-block bg-surface-container-low rounded-md px-2.5 py-0.5 font-label-sm text-[10px] font-bold text-primary uppercase tracking-wider border border-primary/10">
{plan.scanners}
</div>
</div>
<ul className="space-y-2 mb-6 flex-1">
{plan.features.map((feature, idx) => (
<li key={idx} className="flex items-start">
<Check className="w-4 h-4 mr-2.5 shrink-0 mt-0.5 text-primary" strokeWidth={3} />
<span className="font-body-sm text-on-surface text-[12.5px] leading-snug font-medium">{feature}</span>
</li>
))}
</ul>
<button
onClick={() => handleSubscribe(plan.id)}
disabled={loadingPlan === plan.id || isCurrentPlan}
className={`w-full py-2.5 rounded-lg font-label-md text-sm transition-all duration-300 flex items-center justify-center font-bold border-0 cursor-pointer ${isCurrentPlan
? 'bg-surface-container-highest text-on-surface-variant cursor-not-allowed opacity-80'
: 'bg-primary text-on-primary shadow-md hover:opacity-90 hover:shadow-lg active:scale-[0.98]'
}`}
>
{loadingPlan === plan.id ? (
<span className="flex items-center gap-xs"><span className="material-symbols-outlined animate-spin text-[18px]">sync</span> Processing...</span>
) : isCurrentPlan ? 'Current Plan' : plan.id === 'enterprise' ? 'Contact Sales' : 'Get Started'}
{!isCurrentPlan && loadingPlan !== plan.id && <ArrowRight className="w-4 h-4 ml-2 opacity-70" />}
</button>
</div>
);
})}
</div>
<p className="text-center text-[11.5px] text-on-surface-variant mt-6 font-medium">
All payments are processed securely by Razorpay. Platform fee and applicable GST are shown before payment.
</p>
</div>
</div>
);
}