UNPKG

optumflex-subscription-ui

Version:

A comprehensive React UI component library for subscription management, pricing tables, shopping cart, and checkout systems with full customization support

719 lines (688 loc) 38.2 kB
import React, { useState, useCallback, useEffect, useMemo, forwardRef, createElement } from 'react'; import { jsx, Fragment, jsxs } from 'react/jsx-runtime'; // Removed constants import - using inline values // Inline constants const BILLING_CYCLES = ["weekly", "monthly", "quarterly", "halfyearly", "yearly"]; const DEFAULT_BILLING_CYCLE = 'monthly'; const CURRENCY_SYMBOL = '₹'; const DISCOUNT_THRESHOLD = 0; /** * Parse features from API response format */ const parseFeatures = (featuresArr) => { return featuresArr .map((f) => Object.values(f)[0]) .filter((v) => typeof v === 'string' && v.trim() !== ''); }; /** * Parse a single plan from API response */ const parsePlan = (pkg) => ({ id: pkg.package_code_str, title: pkg.package_name, description: pkg.description, prices: pkg.prices, discounted: pkg.discounted, features: parseFeatures(pkg.features), }); /** * Find the minimum price cycle for a plan */ const getMinimumPriceCycle = (prices) => { const validPrices = Object.entries(prices) .filter(([_, price]) => price > 0) .sort(([_, a], [__, b]) => a - b); return validPrices.length > 0 ? validPrices[0][0] : DEFAULT_BILLING_CYCLE; }; /** * Get available billing cycles for a plan */ const getAvailableCycles = (prices) => { return BILLING_CYCLES.filter((cycle) => prices[cycle] > 0); }; /** * Calculate discount information for a plan and cycle */ const calculateDiscountInfo = (plan, cycle) => { const originalPrice = plan.prices[cycle]; const discountedPrice = plan.discounted[cycle]; const isDiscounted = discountedPrice > 0 && discountedPrice < originalPrice; const savings = isDiscounted ? originalPrice - discountedPrice : 0; const savingsPercentage = isDiscounted && originalPrice > 0 ? Math.round((savings / originalPrice) * 100) : 0; return { originalPrice, discountedPrice: isDiscounted ? discountedPrice : originalPrice, savings, savingsPercentage, isDiscounted, }; }; /** * Sort plans based on criteria */ const sortPlans = (plans, sortBy, selectedCycles) => { return [...plans].sort((a, b) => { if (sortBy === 'price-asc') { const aCycle = selectedCycles[a.id] || DEFAULT_BILLING_CYCLE; const bCycle = selectedCycles[b.id] || DEFAULT_BILLING_CYCLE; const aPrice = a.discounted[aCycle] || a.prices[aCycle]; const bPrice = b.discounted[bCycle] || b.prices[bCycle]; return aPrice - bPrice; } else if (sortBy === 'price-desc') { const aCycle = selectedCycles[a.id] || DEFAULT_BILLING_CYCLE; const bCycle = selectedCycles[b.id] || DEFAULT_BILLING_CYCLE; const aPrice = a.discounted[aCycle] || a.prices[aCycle]; const bPrice = b.discounted[bCycle] || b.prices[bCycle]; return bPrice - aPrice; } else if (sortBy === 'name') { const aName = a.title || a.package_name || ''; const bName = b.title || b.package_name || ''; return aName.localeCompare(bName); } return 0; }); }; /** * Generate checkout URL with cart data */ const generateCheckoutUrl = (cart, baseUrl) => { const base64 = typeof window !== 'undefined' ? btoa(unescape(encodeURIComponent(JSON.stringify(cart)))) : ''; return `${baseUrl}/checkout?cart=${encodeURIComponent(base64)}`; }; /** * Handle buy now functionality for a single plan */ const handleBuyNow = (plan, cycle, companyInfo) => { if (!companyInfo?.application) { console.error('Company application URL not configured'); return; } const payload = [{ packageCode: plan.id, duration: cycle, }]; const base64 = typeof window !== 'undefined' ? btoa(unescape(encodeURIComponent(JSON.stringify(payload)))) : ''; const checkoutUrl = `${companyInfo.application}/checkout?cart=${encodeURIComponent(base64)}`; window.open(checkoutUrl, '_blank'); }; /** * Format price with currency symbol */ const formatPrice = (price) => { return `${CURRENCY_SYMBOL}${price}`; }; /** * Calculate total cart value */ const calculateCartTotal = (cart) => { return cart.reduce((total, item) => { const discountInfo = calculateDiscountInfo(item.plan, item.cycle); return total + discountInfo.discountedPrice; }, 0); }; /** * Create checkout payload from cart items */ const createCheckoutPayload = (cart) => { return cart.map(item => ({ packageCode: item.plan.id, duration: item.cycle, })); }; /** * Check if a plan has any discounts */ const hasDiscounts = (plan) => { return BILLING_CYCLES.some(cycle => { const discountInfo = calculateDiscountInfo(plan, cycle); return discountInfo.isDiscounted && discountInfo.savings > DISCOUNT_THRESHOLD; }); }; /** * Get the best discount percentage for a plan */ const getBestDiscountPercentage = (plan) => { let bestDiscount = 0; BILLING_CYCLES.forEach(cycle => { const discountInfo = calculateDiscountInfo(plan, cycle); if (discountInfo.isDiscounted && discountInfo.originalPrice > 0) { const percentage = (discountInfo.savings / discountInfo.originalPrice) * 100; bestDiscount = Math.max(bestDiscount, percentage); } }); return Math.round(bestDiscount); }; /** * Validate plan data */ const validatePlan = (plan) => { return !!(plan?.package_code_str && plan?.package_name && plan?.prices && plan?.discounted && Array.isArray(plan?.features)); }; /** * Filter plans by search query */ const filterPlansBySearch = (plans, searchQuery) => { if (!searchQuery.trim()) return plans; const query = searchQuery.toLowerCase(); return plans.filter(plan => plan.title.toLowerCase().includes(query) || plan.description.toLowerCase().includes(query) || plan.features.some(feature => feature.toLowerCase().includes(query))); }; // Removed API import - using provided data const useSubscription = (options = {}) => { const { initialPlanType = 'subscriptionPlans', initialSortBy = 'default', initialData, config = {}, onError, onSuccess } = options; // State const [subscriptionPlans, setSubscriptionPlans] = useState([]); const [modelPortfolios, setModelPortfolios] = useState([]); const [loading, setLoading] = useState(false); const [error, setError] = useState(null); const [selectedCycles, setSelectedCycles] = useState({}); const [sortBy, setSortBy] = useState(initialSortBy); const [planType, setPlanType] = useState(initialPlanType); // Helper function to find the minimum price cycle const getMinimumPriceCycle = useCallback((prices) => { const validPrices = Object.entries(prices) .filter(([_, price]) => price > 0) .sort(([_, a], [__, b]) => a - b); return validPrices.length > 0 ? validPrices[0][0] : 'monthly'; }, []); // Fetch data const fetchData = useCallback(async () => { setLoading(true); setError(null); try { let data; if (initialData) { // Use provided data instead of fetching data = initialData; } else { // Use provided data only - no API fetching throw new Error('No initial data provided'); } setSubscriptionPlans(data.subscriptionPlans || []); setModelPortfolios(data.modelPortfolios || []); // Set default selected cycle for each plan based on minimum price const defaultCycles = {}; [...(data.subscriptionPlans || []), ...(data.modelPortfolios || [])].forEach(plan => { defaultCycles[plan.id] = getMinimumPriceCycle(plan.prices); }); setSelectedCycles(prevCycles => ({ ...defaultCycles, ...prevCycles // Preserve any existing selections })); onSuccess?.('Data loaded successfully'); } catch (err) { const errorMessage = err instanceof Error ? err.message : 'Failed to fetch data'; setError(errorMessage); onError?.(errorMessage); } finally { setLoading(false); } }, [onSuccess, onError, initialData, getMinimumPriceCycle]); // Load data on mount useEffect(() => { fetchData(); }, [fetchData]); // Computed values const currentPlans = useMemo(() => { return planType === 'subscriptionPlans' ? subscriptionPlans : modelPortfolios; }, [planType, subscriptionPlans, modelPortfolios]); const sortedPlans = useMemo(() => { return sortPlans(currentPlans, sortBy, selectedCycles); }, [currentPlans, sortBy, selectedCycles]); const displayedPlans = useMemo(() => { return sortedPlans; }, [sortedPlans]); // Actions const setSelectedCycle = useCallback((planId, cycle) => { setSelectedCycles(prev => ({ ...prev, [planId]: cycle })); }, []); const refreshData = useCallback(() => { return fetchData(); }, [fetchData]); const getFilteredPlans = useCallback((searchQuery) => { return filterPlansBySearch(currentPlans, searchQuery || ''); }, [currentPlans]); const getSortedPlans = useCallback(() => { return sortedPlans; }, [sortedPlans]); const getDisplayedPlans = useCallback(() => { return displayedPlans; }, [displayedPlans]); const getDiscountInfo = useCallback((plan, cycle) => { return calculateDiscountInfo(plan, cycle); }, []); const getAvailableCyclesForPlan = useCallback((plan) => { return getAvailableCycles(plan.prices); }, []); return { // State subscriptionPlans, modelPortfolios, loading, error, selectedCycles, sortBy, planType, // State setters setPlanType, setSortBy, setSelectedCycle, // Data operations refreshData, getDisplayedPlans, getSortedPlans, getFilteredPlans, // Utility functions getDiscountInfo, getAvailableCycles: getAvailableCyclesForPlan, }; }; /** * @license lucide-react v0.539.0 - ISC * * This source code is licensed under the ISC license. * See the LICENSE file in the root directory of this source tree. */ const toKebabCase = (string) => string.replace(/([a-z0-9])([A-Z])/g, "$1-$2").toLowerCase(); const toCamelCase = (string) => string.replace( /^([A-Z])|[\s-_]+(\w)/g, (match, p1, p2) => p2 ? p2.toUpperCase() : p1.toLowerCase() ); const toPascalCase = (string) => { const camelCase = toCamelCase(string); return camelCase.charAt(0).toUpperCase() + camelCase.slice(1); }; const mergeClasses = (...classes) => classes.filter((className, index, array) => { return Boolean(className) && className.trim() !== "" && array.indexOf(className) === index; }).join(" ").trim(); const hasA11yProp = (props) => { for (const prop in props) { if (prop.startsWith("aria-") || prop === "role" || prop === "title") { return true; } } }; /** * @license lucide-react v0.539.0 - ISC * * This source code is licensed under the ISC license. * See the LICENSE file in the root directory of this source tree. */ var defaultAttributes = { xmlns: "http://www.w3.org/2000/svg", width: 24, height: 24, viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: 2, strokeLinecap: "round", strokeLinejoin: "round" }; /** * @license lucide-react v0.539.0 - ISC * * This source code is licensed under the ISC license. * See the LICENSE file in the root directory of this source tree. */ const Icon = forwardRef( ({ color = "currentColor", size = 24, strokeWidth = 2, absoluteStrokeWidth, className = "", children, iconNode, ...rest }, ref) => createElement( "svg", { ref, ...defaultAttributes, width: size, height: size, stroke: color, strokeWidth: absoluteStrokeWidth ? Number(strokeWidth) * 24 / Number(size) : strokeWidth, className: mergeClasses("lucide", className), ...!children && !hasA11yProp(rest) && { "aria-hidden": "true" }, ...rest }, [ ...iconNode.map(([tag, attrs]) => createElement(tag, attrs)), ...Array.isArray(children) ? children : [children] ] ) ); /** * @license lucide-react v0.539.0 - ISC * * This source code is licensed under the ISC license. * See the LICENSE file in the root directory of this source tree. */ const createLucideIcon = (iconName, iconNode) => { const Component = forwardRef( ({ className, ...props }, ref) => createElement(Icon, { ref, iconNode, className: mergeClasses( `lucide-${toKebabCase(toPascalCase(iconName))}`, `lucide-${iconName}`, className ), ...props }) ); Component.displayName = toPascalCase(iconName); return Component; }; /** * @license lucide-react v0.539.0 - ISC * * This source code is licensed under the ISC license. * See the LICENSE file in the root directory of this source tree. */ const __iconNode$4 = [["path", { d: "M20 6 9 17l-5-5", key: "1gmf2c" }]]; const Check = createLucideIcon("check", __iconNode$4); /** * @license lucide-react v0.539.0 - ISC * * This source code is licensed under the ISC license. * See the LICENSE file in the root directory of this source tree. */ const __iconNode$3 = [["path", { d: "M21 12a9 9 0 1 1-6.219-8.56", key: "13zald" }]]; const LoaderCircle = createLucideIcon("loader-circle", __iconNode$3); /** * @license lucide-react v0.539.0 - ISC * * This source code is licensed under the ISC license. * See the LICENSE file in the root directory of this source tree. */ const __iconNode$2 = [ ["circle", { cx: "8", cy: "21", r: "1", key: "jimo8o" }], ["circle", { cx: "19", cy: "21", r: "1", key: "13723u" }], [ "path", { d: "M2.05 2.05h2l2.66 12.42a2 2 0 0 0 2 1.58h9.78a2 2 0 0 0 1.95-1.57l1.65-7.43H5.12", key: "9zh506" } ] ]; const ShoppingCart = createLucideIcon("shopping-cart", __iconNode$2); /** * @license lucide-react v0.539.0 - ISC * * This source code is licensed under the ISC license. * See the LICENSE file in the root directory of this source tree. */ const __iconNode$1 = [ ["path", { d: "M10 11v6", key: "nco0om" }], ["path", { d: "M14 11v6", key: "outv1u" }], ["path", { d: "M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6", key: "miytrc" }], ["path", { d: "M3 6h18", key: "d0wm0j" }], ["path", { d: "M8 6V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2", key: "e791ji" }] ]; const Trash2 = createLucideIcon("trash-2", __iconNode$1); /** * @license lucide-react v0.539.0 - ISC * * This source code is licensed under the ISC license. * See the LICENSE file in the root directory of this source tree. */ const __iconNode = [ ["path", { d: "M18 6 6 18", key: "1bl5f8" }], ["path", { d: "m6 6 12 12", key: "d8bk6v" }] ]; const X = createLucideIcon("x", __iconNode); // Function to convert billing cycle to titlecase const formatBillingCycle = (cycle) => { return cycle.charAt(0).toUpperCase() + cycle.slice(1); }; const Cart = ({ className = '', cart, isOpen, onClose, onRemoveItem, applicationUrl, colorScheme = {} }) => { const cartTotal = calculateCartTotal(cart); // Helper function to get inline styles for specific elements const getInlineStyles = (type) => { switch (type) { case 'primary-text': return colorScheme.primary ? { color: colorScheme.primary } : {}; case 'gradient-bg': return colorScheme.gradient?.from && colorScheme.gradient?.to ? { background: `linear-gradient(to right, ${colorScheme.gradient.from}, ${colorScheme.gradient.to})` } : {}; default: return {}; } }; const handleCheckout = () => { if (!applicationUrl) { console.error('Application URL not configured'); return; } const payload = cart.map(item => ({ packageCode: item.plan.id, duration: item.cycle, })); const base64 = typeof window !== 'undefined' ? btoa(unescape(encodeURIComponent(JSON.stringify(payload)))) : ''; const checkoutUrl = `${applicationUrl}/checkout?cart=${encodeURIComponent(base64)}`; window.open(checkoutUrl, '_blank'); }; return (jsx(Fragment, { children: jsxs("div", { className: `fixed inset-0 z-50 overflow-hidden transition-opacity duration-300 ${isOpen ? 'opacity-100' : 'opacity-0 pointer-events-none'}`, children: [jsx("div", { className: `absolute inset-0 bg-black transition-opacity duration-300 ${isOpen ? 'bg-opacity-50' : 'bg-opacity-0'}`, onClick: onClose }), jsx("div", { className: `absolute right-0 top-0 h-full w-full max-w-md bg-white shadow-2xl transform transition-transform duration-300 ease-in-out ${isOpen ? 'translate-x-0' : 'translate-x-full'}`, children: jsxs("div", { className: "flex flex-col h-full", children: [jsxs("div", { className: "flex items-center justify-between p-6 border-b", style: { borderColor: colorScheme.border || '#E5E7EB' }, children: [jsx("h3", { className: "text-lg font-semibold", style: { color: colorScheme.text || '#1F2937' }, children: "Shopping Cart" }), jsx("button", { onClick: onClose, className: "transition-colors", style: { color: colorScheme.primary || '#6B7280' }, children: jsx(X, { className: "w-6 h-6" }) })] }), jsx("div", { className: "flex-1 overflow-y-auto p-6", children: cart.length === 0 ? (jsxs("div", { className: "text-center py-8", children: [jsx(ShoppingCart, { className: "w-16 h-16 mx-auto mb-4", style: { color: colorScheme.primary || '#D1D5DB' } }), jsx("p", { style: { color: colorScheme.text || '#6B7280' }, children: "Your cart is empty" })] })) : (jsx("div", { className: "space-y-4", children: cart.map((item) => { const { originalPrice, discountedPrice, savings, isDiscounted } = calculateDiscountInfo(item.plan, item.cycle); return (jsxs("div", { className: "flex items-center justify-between p-4 border rounded-lg", style: { borderColor: colorScheme.border || '#E5E7EB' }, children: [jsxs("div", { className: "flex-1", children: [jsx("h4", { className: "font-medium", style: { color: colorScheme.text || '#1F2937' }, children: item.plan.package_name || item.plan.title }), jsxs("p", { className: "text-sm", style: { color: colorScheme.primary || '#3B82F6' }, children: [formatBillingCycle(item.cycle), " - ", formatPrice(discountedPrice)] }), isDiscounted && (jsxs("div", { className: "text-xs mt-1", children: [jsx("del", { style: { color: '#9CA3AF' }, children: formatPrice(originalPrice) }), jsxs("span", { style: { color: '#16A34A', marginLeft: 8 }, children: ["Save ", formatPrice(savings)] })] }))] }), jsx("button", { onClick: () => onRemoveItem(item.plan.id), className: "text-red-500 hover:text-red-700 transition-colors", children: jsx(Trash2, { className: "w-5 h-5" }) })] }, `${item.plan.id}-${item.cycle}`)); }) })) }), cart.length > 0 && (jsxs("div", { className: "border-t p-6", style: { borderColor: colorScheme.border || '#E5E7EB' }, children: [jsxs("div", { className: "flex items-center justify-between mb-4", children: [jsx("span", { className: "text-lg font-semibold", style: { color: colorScheme.text || '#1F2937' }, children: "Total:" }), jsxs("span", { className: "text-lg font-bold", style: getInlineStyles('primary-text'), children: ["\u20B9", cartTotal] })] }), jsx("button", { onClick: handleCheckout, className: "w-full text-white py-3 px-4 rounded-lg font-semibold transition-all transform hover:scale-[1.02]", style: getInlineStyles('gradient-bg'), children: "Proceed to Checkout" })] }))] }) })] }) })); }; const Subscription = ({ apiData, config = {}, applicationUrl, className = '', colorScheme = {}, renderPlanCard, renderHeader, renderCart }) => { const { loading, error, selectedCycles, sortBy, planType, setPlanType, setSortBy, setSelectedCycle, getDisplayedPlans, getDiscountInfo, getAvailableCycles, } = useSubscription({ initialData: apiData, config }); const displayedPlans = getDisplayedPlans(); // Plain state management instead of Context const [cart, setCart] = useState([]); const [isCartOpen, setIsCartOpen] = useState(false); // Cart functions const addToCart = (plan, cycle) => { const normalizedId = resolvePlanId(plan); const normalizedPlan = { ...plan, id: normalizedId }; setCart(prev => { const existingIndex = prev.findIndex(item => resolvePlanId(item.plan) === normalizedId); if (existingIndex >= 0) { // Update existing item const updated = [...prev]; updated[existingIndex] = { plan: normalizedPlan, cycle }; return updated; } // Add new item return [...prev, { plan: normalizedPlan, cycle }]; }); }; const removeFromCart = (planId) => { setCart(prev => prev.filter(item => resolvePlanId(item.plan) !== planId)); }; const isPlanInCart = (planId) => { return cart.some(item => resolvePlanId(item.plan) === planId); }; const cartItemCount = cart.length; const openCart = () => setIsCartOpen(true); const closeCart = () => setIsCartOpen(false); // Function to handle direct checkout for a single plan const handleBuyNow$1 = (plan, cycle) => { handleBuyNow(plan, cycle, { application: applicationUrl }); }; // Function to convert billing cycle to titlecase const formatBillingCycle = (cycle) => { return cycle.charAt(0).toUpperCase() + cycle.slice(1); }; // Helper to resolve a stable plan id even if API shape differs const resolvePlanId = (plan) => { return (plan?.id ?? plan?.package_code_str ?? plan?.package_code ?? plan?.code ?? ''); }; // Helper function to get color scheme CSS variables const getColorSchemeStyles = () => { const styles = {}; // Set CSS custom properties for colors if (colorScheme.primary) { styles['--primary-color'] = colorScheme.primary; styles['--primary-color-10'] = `${colorScheme.primary}1a`; // 10% opacity styles['--primary-color-5'] = `${colorScheme.primary}0d`; // 5% opacity styles['--primary-color-90'] = `${colorScheme.primary}e6`; // 90% opacity } if (colorScheme.secondary) styles['--secondary-color'] = colorScheme.secondary; if (colorScheme.accent) styles['--accent-color'] = colorScheme.accent; if (colorScheme.background) styles['--background-color'] = colorScheme.background; if (colorScheme.text) styles['--text-color'] = colorScheme.text; if (colorScheme.border) styles['--border-color'] = colorScheme.border; if (colorScheme.gradient?.from) styles['--gradient-from'] = colorScheme.gradient.from; if (colorScheme.gradient?.to) styles['--gradient-to'] = colorScheme.gradient.to; return styles; }; // Helper function to get inline styles for specific elements const getInlineStyles = (type) => { switch (type) { case 'primary-bg': return colorScheme.primary ? { backgroundColor: colorScheme.primary } : {}; case 'primary-text': return colorScheme.primary ? { color: colorScheme.primary } : {}; case 'primary-border': return colorScheme.primary ? { borderColor: colorScheme.primary } : {}; case 'gradient-bg': return colorScheme.gradient?.from && colorScheme.gradient?.to ? { background: `linear-gradient(to right, ${colorScheme.gradient.from}, ${colorScheme.gradient.to})` } : {}; case 'primary-bg-10': return colorScheme.primary ? { backgroundColor: `${colorScheme.primary}1a` } : {}; case 'primary-bg-5': return colorScheme.primary ? { backgroundColor: `${colorScheme.primary}0d` } : {}; default: return {}; } }; // Default Header Component (Stocklution Style) const DefaultHeader = ({ planType, setPlanType, sortBy, setSortBy }) => { const hasModelPortfolios = apiData.modelPortfolios && apiData.modelPortfolios.length > 0; return (jsxs("div", { className: "flex flex-col md:flex-row md:items-center md:justify-between mb-8 gap-4", children: [jsx("div", { className: "flex-1", children: jsx("div", { className: "w-full max-w-md", children: jsxs("div", { className: `grid p-1 rounded-full ${hasModelPortfolios ? 'grid-cols-2' : 'grid-cols-1'}`, style: getInlineStyles('primary-bg-10'), children: [jsx("button", { className: `px-4 py-2 rounded-full text-sm font-medium transition-all ${planType === 'subscriptionPlans' ? 'shadow-sm' : ''}`, style: { ...(planType === 'subscriptionPlans' ? { backgroundColor: 'white', color: colorScheme.primary || '#3B82F6' } : { color: colorScheme.primary || '#3B82F6' }) }, onClick: () => setPlanType('subscriptionPlans'), children: "Subscription Plans" }), hasModelPortfolios && (jsx("button", { className: `px-4 py-2 rounded-full text-sm font-medium transition-all ${planType === 'modelPortfolios' ? 'shadow-sm' : ''}`, style: { ...(planType === 'modelPortfolios' ? { backgroundColor: 'white', color: colorScheme.primary || '#3B82F6' } : { color: colorScheme.primary || '#3B82F6' }) }, onClick: () => setPlanType('modelPortfolios'), children: "Model Portfolios" }))] }) }) }), jsxs("div", { className: "flex items-center gap-4", children: [jsx("div", { children: jsxs("select", { className: "border rounded px-3 py-2 text-sm", style: { backgroundColor: 'white', borderColor: colorScheme.primary || '#3B82F6', color: colorScheme.primary || '#3B82F6' }, value: sortBy, onChange: (e) => setSortBy(e.target.value), children: [jsx("option", { value: "default", children: "Sort by" }), jsx("option", { value: "price-asc", children: "Price: Low to High" }), jsx("option", { value: "price-desc", children: "Price: High to Low" }), jsx("option", { value: "name", children: "Name" })] }) }), cartItemCount > 0 && (jsxs("button", { onClick: openCart, className: "relative text-white rounded-full px-4 py-2 shadow-lg hover:shadow-xl transition-all duration-200 transform hover:scale-105 flex items-center gap-2", style: getInlineStyles('gradient-bg'), children: [jsx(ShoppingCart, { className: "w-4 h-4" }), jsx("span", { className: "text-sm font-semibold", children: "Cart" }), jsx("span", { className: "bg-red-500 text-white text-xs rounded-full w-5 h-5 flex items-center justify-center font-bold border border-white", children: cartItemCount })] }))] })] })); }; // Default Plan Card Component (Stocklution Style) const DefaultPlanCard = ({ plan, availableCycles, selectedCycle, isSelected, discountInfo, onCycleSelect, onAddToCart, isInCart, onBuyNow }) => (jsxs("div", { className: "border shadow-lg rounded-2xl h-full flex flex-col transition-all hover:shadow-2xl", children: [jsxs("div", { className: "p-6 border-b rounded-t-2xl", style: getInlineStyles('primary-bg-5'), children: [jsxs("div", { className: "flex items-center justify-between mb-1", children: [jsx("h3", { className: "text-2xl font-bold leading-tight", style: getInlineStyles('primary-text'), children: plan.package_name || plan.title }), jsx("div", { className: "text-right", children: (() => { const discountInfo = getDiscountInfo(plan, selectedCycle); return (jsx("div", { className: "text-lg font-bold", style: getInlineStyles('primary-text'), children: discountInfo.isDiscounted ? (jsxs("div", { className: "flex flex-col items-end", children: [jsxs("span", { className: "text-green-600", children: ["\u20B9", discountInfo.discountedPrice] }), jsxs("del", { className: "text-sm text-muted-foreground font-normal", children: ["\u20B9", discountInfo.originalPrice] })] })) : (jsxs("span", { children: ["\u20B9", discountInfo.originalPrice] })) })); })() })] }), jsx("p", { className: "text-base text-muted-foreground mb-4", children: plan.description }), jsx("div", { className: "flex flex-col gap-2 mt-2", children: availableCycles.map((cycle) => { const cycleDiscountInfo = getDiscountInfo(plan, cycle); const original = plan.prices[cycle]; const discounted = plan.discounted[cycle]; const isDiscounted = discounted > 0 && discounted < original; const savings = isDiscounted ? original - discounted : 0; const cycleIsSelected = cycle === selectedCycle; return (jsxs("div", { className: `flex items-center justify-between rounded-md px-3 py-2 border mb-1 text-sm cursor-pointer transition-all ${cycleIsSelected ? 'ring-2' : ''}`, style: { ...(cycleIsSelected ? { ...getInlineStyles('primary-bg-10'), ...getInlineStyles('primary-border') } : {}) }, onClick: () => onCycleSelect(cycle), tabIndex: 0, role: "button", "aria-pressed": cycleIsSelected, onKeyDown: e => { if (e.key === 'Enter' || e.key === ' ') onCycleSelect(cycle); }, children: [jsx("span", { className: "w-10 font-medium text-foreground", children: formatBillingCycle(cycle) }), jsx("span", { className: "flex items-center gap-2", children: isDiscounted ? (jsxs(Fragment, { children: [jsxs("del", { className: "line-through text-muted-foreground", children: ["\u20B9", original] }), jsxs("span", { className: "text-green-600 font-bold", children: ["\u20B9", discounted] })] })) : (jsxs("span", { className: "font-bold", children: ["\u20B9", original] })) }), savings > 0 && (jsx("span", { className: "text-sm text-green-600 text-right", children: config.showSavingsAsPercentage ? `Save ${cycleDiscountInfo.savingsPercentage}%` : `Save ₹${savings}` }))] }, cycle)); }) })] }), jsxs("div", { className: "p-6 flex-1 flex flex-col rounded-b-2xl bg-background", children: [jsx("h4", { className: "font-medium mb-4 text-lg", children: "What's included:" }), jsx("ul", { className: "space-y-3 mb-6", children: (() => { // Parse features using the provided logic const parseFeatures = (featuresArr) => { return featuresArr .map((f) => Object.values(f)[0]) .filter((v) => typeof v === 'string' && v.trim() !== ''); }; const validFeatures = parseFeatures(plan.features); if (validFeatures.length === 0) { return jsx("li", { className: "text-muted-foreground text-sm", children: "No features listed." }); } return validFeatures.map((feature, idx) => (jsxs("li", { className: "flex items-start gap-3", children: [jsx("div", { className: "h-5 w-5 rounded-full flex items-center justify-center mt-0.5 flex-shrink-0", style: getInlineStyles('primary-bg-10'), children: jsx(Check, { className: "h-3.5 w-3.5", style: getInlineStyles('primary-text') }) }), jsx("span", { className: "text-sm", children: feature })] }, idx))); })() }), config.showCart !== false ? (jsx("button", { className: `w-full text-base h-10 font-semibold rounded-lg shadow-md transition-all flex items-center justify-center text-white transform hover:scale-[1.02] ${isInCart ? 'border-2 border-red-800 shadow-xl font-bold hover:bg-red-700' : ''}`, style: isInCart ? { backgroundColor: '#dc2626' } : getInlineStyles('gradient-bg'), onClick: isInCart ? () => removeFromCart(resolvePlanId(plan)) : onAddToCart, children: isInCart ? 'Remove from Cart' : 'Add to Cart' })) : (jsx("button", { className: "w-full text-base h-10 flex items-center justify-center font-semibold py-3 rounded-lg shadow-md transition-all text-white transform hover:scale-[1.02]", style: getInlineStyles('gradient-bg'), onClick: () => onBuyNow(plan, selectedCycle), children: "Buy Now" }))] })] })); if (loading) { return (jsx("div", { className: `w-full ${className}`, children: jsx("div", { className: "flex items-center justify-center py-20", children: jsxs("div", { className: "text-center", children: [jsx(LoaderCircle, { className: "h-12 w-12 animate-spin mx-auto mb-4", style: { color: colorScheme.primary || '#3B82F6' } }), jsx("p", { className: "text-muted-foreground", children: "Loading subscription plans..." })] }) }) })); } if (error) { return (jsx("div", { className: `w-full ${className}`, children: jsx("div", { className: "flex items-center justify-center py-20", children: jsxs("div", { className: "text-center", children: [jsxs("p", { className: "text-destructive mb-4", children: ["Error: ", error] }), jsx("button", { onClick: () => window.location.reload(), className: "px-4 py-2 text-white rounded-lg transition-colors", style: getInlineStyles('primary-bg'), children: "Try Again" })] }) }) })); } return (jsxs("div", { className: `w-full ${className}`, style: getColorSchemeStyles(), children: [renderHeader ? (renderHeader({ planType, setPlanType, sortBy, setSortBy })) : (jsx(DefaultHeader, { planType: planType, setPlanType: setPlanType, sortBy: sortBy, setSortBy: setSortBy })), jsx("div", { className: "grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-8", children: displayedPlans.map((plan, index) => { const availableCycles = getAvailableCycles(plan); // Use a robust plan id const planId = resolvePlanId(plan) || `plan-${index}`; // Helper function to find minimum price cycle const getMinimumPriceCycle = (prices) => { const validPrices = Object.entries(prices) .filter(([_, price]) => price > 0) .sort(([_, a], [__, b]) => a - b); return validPrices.length > 0 ? validPrices[0][0] : 'monthly'; }; // Use selected cycle or fallback to minimum price cycle const selectedCycle = selectedCycles[planId] || getMinimumPriceCycle(plan.prices); const discountInfo = getDiscountInfo(plan, selectedCycle); const isInCart = isPlanInCart(planId); const planCardProps = { plan, availableCycles, selectedCycle, isSelected: selectedCycles[planId] === selectedCycle || (!selectedCycles[planId] && selectedCycle === getMinimumPriceCycle(plan.prices)), discountInfo, onCycleSelect: (cycle) => { setSelectedCycle(planId, cycle); // If plan is in cart, update it with new cycle if (isPlanInCart(planId)) { removeFromCart(planId); addToCart(plan, cycle); } }, onAddToCart: () => addToCart(plan, selectedCycle), isInCart, onBuyNow: handleBuyNow$1 }; return (jsx("div", { children: renderPlanCard ? (jsx("div", { children: renderPlanCard(planCardProps) }, `custom-plan-${planId}`)) : (jsx(DefaultPlanCard, { ...planCardProps }, `default-plan-${planId}`)) }, planId)); }) }), renderCart && (jsx("div", { children: React.isValidElement(renderCart({ cart, isCartOpen, closeCart, removeFromCart })) ? renderCart({ cart, isCartOpen, closeCart, removeFromCart }) : jsx("div", { children: "Invalid cart component" }) }, "custom-cart")), !renderCart && (jsx(Cart, { cart: cart, isOpen: isCartOpen, onClose: closeCart, onRemoveItem: removeFromCart, applicationUrl: applicationUrl, colorScheme: colorScheme }, "default-cart"))] })); }; export { Cart, Subscription, calculateCartTotal, calculateDiscountInfo, createCheckoutPayload, filterPlansBySearch, formatPrice, generateCheckoutUrl, getAvailableCycles, getBestDiscountPercentage, getMinimumPriceCycle, handleBuyNow, hasDiscounts, parseFeatures, parsePlan, sortPlans, useSubscription, validatePlan }; //# sourceMappingURL=index.esm.js.map