Spaces:
Running on CPU Upgrade
Running on CPU Upgrade
Download frontend/src/components/PricingSection.jsx from larxius/SentinelScan-WSS: direct link, hf CLI and curl.
- Browser
- Download file 24.2 kB
-
https://huggingface.co/spaces/larxius/SentinelScan-WSS/resolve/main/frontend/src/components/PricingSection.jsx
- Command line
-
hf download hf://spaces/larxius/SentinelScan-WSS/frontend/src/components/PricingSection.jsx
-
curl -L -o PricingSection.jsx https://huggingface.co/spaces/larxius/SentinelScan-WSS/resolve/main/frontend/src/components/PricingSection.jsx
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> | |
| ); | |
| } | |