UNPKG

optumflex-subscription-core

Version:

Core logic and utilities for subscription management, pricing calculations, and data processing - framework agnostic

856 lines (849 loc) 28.8 kB
import { useState, useEffect, useCallback, useMemo } from 'react'; // Inline constants const BILLING_CYCLES = ["weekly", "monthly", "quarterly", "halfyearly", "yearly"]; const DEFAULT_BILLING_CYCLE = 'monthly'; const CURRENCY_SYMBOL = '₹'; const DISCOUNT_THRESHOLD = 0; // Package state management let isPackageInitialized = false; let packageValidationError = null; let packageValidationData = null; let apiKey = null; let validationEndpoint = 'https://api.optumflex.com/validate-key'; /** * Set API key for the package (call this once at app startup) */ const setApiKey = (key, endpoint) => { apiKey = key; if (endpoint) { validationEndpoint = endpoint; } // Reset initialization state when API key changes isPackageInitialized = false; packageValidationError = null; packageValidationData = null; }; /** * Validate API key for the current domain */ const validateApiKey = async (apiKey, domain, validationEndpoint) => { try { // ✅ Mock response (pretend validation succeeded) return new Promise((resolve) => setTimeout(() => { resolve({ isValid: true, domain, expiresAt: new Date(Date.now() + 60 * 60 * 1000).toISOString(), // 1 hr later }); }, 500) // simulate latency ); // ---- real API call would go below, but it's mocked for now ---- /* const response = await fetch(validationEndpoint, { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-api-key': apiKey, 'X-Domain': domain, }, body: JSON.stringify({ timestamp: new Date().toISOString(), }), }); if (!response.ok) { throw new Error(`HTTP ${response.status}: ${response.statusText}`); } const data = await response.json(); return { isValid: data.valid === true, error: data.error, domain: data.domain, expiresAt: data.expiresAt, }; */ } catch (error) { console.error('API key validation failed:', error); return { isValid: false, error: error instanceof Error ? error.message : 'Validation failed', }; } }; /** * Get current domain */ const getCurrentDomain = () => { if (typeof window !== 'undefined') { return window.location.hostname; } return 'localhost'; }; /** * Initialize the package automatically (internal function) */ const initializePackage = async () => { // If already initialized, return success if (isPackageInitialized) { return { success: true }; } // Check if API key is set if (!apiKey) { const error = 'API key not set. Call setApiKey() before using the package.'; packageValidationError = error; return { success: false, error }; } try { const domain = getCurrentDomain(); const validation = await validateApiKey(apiKey, domain, validationEndpoint); if (!validation.isValid) { packageValidationError = validation.error || 'Invalid API key'; return { success: false, error: packageValidationError }; } // Package is now initialized and validated isPackageInitialized = true; packageValidationData = { domain: validation.domain, expiresAt: validation.expiresAt, }; return { success: true }; } catch (error) { const errorMessage = error instanceof Error ? error.message : 'Unknown error'; packageValidationError = errorMessage; return { success: false, error: errorMessage }; } }; /** * Ensure package is initialized before executing any function */ const ensureInitialized = async () => { const result = await initializePackage(); if (!result.success) { throw new Error(result.error || 'Package initialization failed'); } }; /** * Get package validation status */ const getPackageStatus = () => { return { isInitialized: isPackageInitialized, error: packageValidationError || undefined, ...packageValidationData, }; }; /** * 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 || ''; const bName = b.title || ''; 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))); }; /** * Get period label for display */ const getPeriodLabel = (period) => { const periodMap = { weekly: 'Weekly', monthly: 'Monthly', quarterly: 'Quarterly', halfyearly: 'Half Yearly', yearly: 'Yearly' }; return periodMap[period] || period; }; /** * Get icon for plan based on index */ const getIcon = (index) => { const icons = [ '🚀', // Rocket for first plan '⭐', // Star for second plan '💎', // Diamond for third plan '🏆', // Trophy for fourth plan '🔥', // Fire for fifth plan '⚡', // Lightning for sixth plan '🌟', // Sparkle for seventh plan '🎯', // Target for eighth plan '💫', // Dizzy for ninth plan '✨' // Sparkles for tenth plan ]; return icons[index % icons.length]; }; /** * Get icon color class based on index */ const getIconColor = (index) => { const colors = [ 'text-blue-500', 'text-green-500', 'text-purple-500', 'text-orange-500', 'text-red-500', 'text-pink-500', 'text-indigo-500', 'text-teal-500', 'text-yellow-500', 'text-cyan-500' ]; return colors[index % colors.length]; }; /** * Handle period selection for a plan */ const handlePeriodSelect = (planId, period, selectedPeriods, setSelectedPeriods) => { setSelectedPeriods({ ...selectedPeriods, [planId]: period }); }; /** * Get default billing cycles for all plans */ const getDefaultCycles = (plans) => { const defaultCycles = {}; plans.forEach(plan => { defaultCycles[plan.id] = getMinimumPriceCycle(plan.prices); }); return defaultCycles; }; /** * Format currency with proper symbol and formatting */ const formatCurrency = (amount, currency = '₹') => { return `${currency}${amount.toLocaleString('en-IN')}`; }; /** * Calculate savings amount and percentage */ const calculateSavings = (originalPrice, discountedPrice) => { const savings = originalPrice - discountedPrice; const percentage = originalPrice > 0 ? (savings / originalPrice) * 100 : 0; return { amount: savings, percentage: Math.round(percentage) }; }; /** * Process raw API response and convert to subscription data format */ const processSubscriptionData = (apiResponse) => { try { // Parse features from array of objects const parseFeatures = (featuresArr) => { return featuresArr .map((f) => Object.values(f)[0]) .filter((v) => typeof v === 'string' && v.trim() !== ''); }; 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), }); const subscriptionPlans = (apiResponse?.subscriptionPlans || []).map(parsePlan); const modelPortfolios = (apiResponse?.modelPortfolios || []).map(parsePlan); return { subscriptionPlans, modelPortfolios, }; } catch (error) { console.error('Error processing subscription data:', error); return { subscriptionPlans: [], modelPortfolios: [], }; } }; /** * Get subscription data with default cycles set */ const getSubscriptionData = (apiResponse) => { const { subscriptionPlans, modelPortfolios } = processSubscriptionData(apiResponse); // Set default selected cycle for each plan based on minimum price const defaultCycles = {}; [...subscriptionPlans, ...modelPortfolios].forEach(plan => { defaultCycles[plan.id] = getMinimumPriceCycle(plan.prices); }); return { subscriptionPlans, modelPortfolios, defaultCycles, }; }; /** * Create subscription state from API response */ const createSubscriptionState = (apiResponse, options = {}) => { const { subscriptionPlans, modelPortfolios, defaultCycles } = getSubscriptionData(apiResponse); return { subscriptionPlans, modelPortfolios, selectedCycles: defaultCycles, planType: options.initialPlanType || 'subscriptionPlans', sortBy: options.initialSortBy || 'default', }; }; /** * Update selected cycles for plans */ const updateSelectedCycles = (currentCycles, planId, cycle) => { return { ...currentCycles, [planId]: cycle, }; }; /** * Get sorted and filtered plans for display */ const getDisplayPlans = (plans, sortBy, selectedCycles, searchQuery) => { let filteredPlans = plans; // Apply search filter if provided if (searchQuery) { filteredPlans = filterPlansBySearch(plans, searchQuery); } // Apply sorting return sortPlans(filteredPlans, sortBy, selectedCycles); }; /** * Generate checkout URL for a single plan */ const generateSinglePlanCheckoutUrl = (plan, cycle, companyInfo) => { const payload = [{ packageCode: plan.id, duration: cycle, }]; const base64 = typeof window !== 'undefined' ? btoa(unescape(encodeURIComponent(JSON.stringify(payload)))) : ''; return `${companyInfo.application}/checkout?cart=${encodeURIComponent(base64)}`; }; /** * Get plan statistics */ const getPlanStatistics = (plans) => { if (plans.length === 0) { return { totalPlans: 0, plansWithDiscounts: 0, averagePrice: 0, priceRange: { min: 0, max: 0 }, }; } const prices = []; let plansWithDiscounts = 0; plans.forEach(plan => { BILLING_CYCLES.forEach(cycle => { const price = plan.prices[cycle]; if (price > 0) { prices.push(price); } }); if (hasDiscounts(plan)) { plansWithDiscounts++; } }); const averagePrice = prices.length > 0 ? prices.reduce((a, b) => a + b, 0) / prices.length : 0; const priceRange = { min: Math.min(...prices), max: Math.max(...prices), }; return { totalPlans: plans.length, plansWithDiscounts, averagePrice: Math.round(averagePrice), priceRange, }; }; /** * Validate API response structure */ const validateApiResponse = (response) => { return !!(response && (response.subscriptionPlans || response.modelPortfolios) && Array.isArray(response.subscriptionPlans || []) && Array.isArray(response.modelPortfolios || [])); }; /** * Transform API response to component-ready format */ const transformApiResponse = (apiResponse) => { if (!validateApiResponse(apiResponse)) { return { isValid: false, error: 'Invalid API response structure', }; } try { const data = getSubscriptionData(apiResponse); return { isValid: true, data, }; } catch (error) { return { isValid: false, error: error instanceof Error ? error.message : 'Failed to process data', }; } }; /** * Check if a billing cycle is selected for a specific plan */ const isSelected = (selectedCycles, planId, cycle) => { return selectedCycles[planId] === cycle; }; /** * Get displayed plans with sorting and filtering applied */ const getDisplayedPlans = (plans, sortBy, selectedCycles, searchQuery) => { let filteredPlans = plans; if (searchQuery) { filteredPlans = filterPlansBySearch(plans, searchQuery); } return sortPlans(filteredPlans, sortBy, selectedCycles); }; /** * Scroll to a specific section on the page */ const scrollToSection = (sectionId) => { const element = document.getElementById(sectionId); if (element) { element.scrollIntoView({ behavior: 'smooth', block: 'start' }); // Optionally focus the first interactive element inside the section const firstButton = element.querySelector('button, [tabindex]:not([tabindex="-1"])'); if (firstButton) firstButton.focus(); } }; /** * Handle keyboard events for accessibility */ const handleKeyDown = (event, action) => { if (event.key === 'Enter' || event.key === ' ') { event.preventDefault(); action(); } }; /** * Get CSS classes for cycle selection styling */ const getCycleSelectionClasses = (isSelected, baseClasses = '') => { const selectedClasses = 'bg-primary/10 border-primary ring-2 ring-primary/30'; const defaultClasses = 'bg-white border-gray-100 hover:bg-primary/5'; return `${baseClasses} ${isSelected ? selectedClasses : defaultClasses}`; }; /** * Generate benefits data structure */ const generateBenefitsData = (benefits) => { return benefits.map((benefit, index) => ({ id: index, ...benefit })); }; /** * Generate FAQ data structure */ const generateFAQData = (faqs) => { return faqs.map((faq, index) => ({ id: index, ...faq })); }; const useSubscriptionData = (options = {}) => { const { initialPlanType = 'subscriptionPlans', initialSortBy = 'default', onError, onSuccess, enableCart = false, appUrl, apiKey } = options; // State management const [subscriptionPlans, setSubscriptionPlans] = useState([]); const [modelPortfolios, setModelPortfolios] = useState([]); const [selectedCycles, setSelectedCycles] = useState({}); const [planType, setPlanType] = useState(initialPlanType); const [sortBy, setSortBy] = useState(initialSortBy); const [loading, setLoading] = useState(false); const [error, setError] = useState(null); // Cart state (only when enabled) const [cart, setCart] = useState([]); const [showCart, setShowCart] = useState(false); const [isInitialized, setIsInitialized] = useState(false); // Initialize package on first use useEffect(() => { const initPackage = async () => { try { // Set API key if provided if (apiKey) { setApiKey(apiKey); } await ensureInitialized(); setIsInitialized(true); setError(null); } catch (err) { const errorMessage = err instanceof Error ? err.message : 'Package initialization failed'; setError(errorMessage); onError?.(errorMessage); } }; initPackage(); }, [onError, apiKey]); // Process API response const processApiResponse = useCallback(async (apiResponse) => { try { // Ensure package is initialized before processing await ensureInitialized(); const result = processSubscriptionData(apiResponse); setSubscriptionPlans(result.subscriptionPlans); setModelPortfolios(result.modelPortfolios); setError(null); onSuccess?.('Data processed successfully'); } catch (err) { const errorMessage = err instanceof Error ? err.message : 'Failed to process data'; setError(errorMessage); onError?.(errorMessage); } }, [onError, onSuccess]); // Set default cycles when plans are loaded useEffect(() => { if (subscriptionPlans.length > 0 || modelPortfolios.length > 0) { const allPlans = [...subscriptionPlans, ...modelPortfolios]; const defaultCycles = {}; allPlans.forEach(plan => { // Only set default if not already set if (!selectedCycles[plan.id]) { defaultCycles[plan.id] = getMinimumPriceCycle(plan.prices); } }); // Only update if we have new defaults to set if (Object.keys(defaultCycles).length > 0) { setSelectedCycles(prev => ({ ...prev, ...defaultCycles })); } } }, [subscriptionPlans, modelPortfolios, selectedCycles]); // Get current plans based on plan type const getCurrentPlans = useCallback(() => { return planType === 'subscriptionPlans' ? subscriptionPlans : modelPortfolios; }, [planType, subscriptionPlans, modelPortfolios]); // Get displayed plans with sorting and filtering const getDisplayedPlans = useCallback((searchQuery) => { const currentPlans = getCurrentPlans(); let filteredPlans = currentPlans; if (searchQuery) { filteredPlans = filterPlansBySearch(currentPlans, searchQuery); } return sortPlans(filteredPlans, sortBy, selectedCycles); }, [getCurrentPlans, sortBy, selectedCycles]); // Set selected cycle for a plan const setSelectedCycle = useCallback((planId, cycle) => { setSelectedCycles(prev => ({ ...prev, [planId]: cycle })); }, []); // Get discount info for a plan and cycle const getDiscountInfo = useCallback((plan, cycle) => { return calculateDiscountInfo(plan, cycle); }, []); // Get available cycles for a plan const getAvailableCycles$1 = useCallback((plan) => { return getAvailableCycles(plan.prices); }, []); // Get formatted price for a plan and cycle const getPlanPrice = useCallback((plan, cycle) => { const discountInfo = calculateDiscountInfo(plan, cycle); if (discountInfo.isDiscounted) { return `${formatPrice(discountInfo.discountedPrice)}`; } return formatPrice(discountInfo.originalPrice); }, []); // Get plan statistics const getPlanStatistics = useCallback(() => { const currentPlans = getCurrentPlans(); const totalPlans = currentPlans.length; const plansWithDiscounts = currentPlans.filter(plan => getAvailableCycles$1(plan).some(cycle => getDiscountInfo(plan, cycle).isDiscounted)).length; const allPrices = currentPlans.flatMap(plan => getAvailableCycles$1(plan).map(cycle => getDiscountInfo(plan, cycle).discountedPrice)); const averagePrice = allPrices.length > 0 ? allPrices.reduce((a, b) => a + b, 0) / allPrices.length : 0; const priceRange = { min: allPrices.length > 0 ? Math.min(...allPrices) : 0, max: allPrices.length > 0 ? Math.max(...allPrices) : 0, }; return { totalPlans, plansWithDiscounts, averagePrice: Math.round(averagePrice), priceRange, }; }, [getCurrentPlans, getAvailableCycles$1, getDiscountInfo]); // Cart functions (only when enabled) const addToCart = useCallback((plan, cycle) => { if (!enableCart) return; // Check if plan already exists in cart const existingItemIndex = cart.findIndex(item => item.plan.id === plan.id); if (existingItemIndex !== -1) { // Update existing item with new cycle setCart(cart.map((item, index) => index === existingItemIndex ? { plan, cycle } : item)); } else { // Add new item setCart([...cart, { plan, cycle }]); } }, [cart, enableCart]); const removeFromCart = useCallback((planId, cycle) => { if (!enableCart) return; setCart(cart.filter(item => !(item.plan.id === planId && item.cycle === cycle))); }, [cart, enableCart]); const updateCartItemCycle = useCallback((planId, newCycle) => { if (!enableCart) return; setCart(cart.map(item => item.plan.id === planId ? { ...item, cycle: newCycle } : item)); }, [cart, enableCart]); const clearCart = useCallback(() => { if (!enableCart) return; setCart([]); setShowCart(false); }, [enableCart]); const cartTotal = useMemo(() => calculateCartTotal(cart), [cart]); const cartItemCount = useMemo(() => cart.length, [cart]); // Generate checkout URL for single plan const generateSinglePlanCheckoutUrl = useCallback((plan, cycle) => { if (!appUrl) return '#'; const checkoutPayload = [{ packageCode: plan.id, duration: cycle, }]; const base64 = btoa(unescape(encodeURIComponent(JSON.stringify(checkoutPayload)))); return `${appUrl}/checkout?cart=${encodeURIComponent(base64)}`; }, [appUrl]); // Handle cart checkout const handleCheckout = useCallback(() => { if (!cart || cart.length === 0 || !appUrl) return; const checkoutPayload = cart.map(item => ({ packageCode: item.plan.id, duration: item.cycle, })); const base64 = btoa(unescape(encodeURIComponent(JSON.stringify(checkoutPayload)))); const checkoutUrl = `${appUrl}/checkout?cart=${encodeURIComponent(base64)}`; window.open(checkoutUrl, '_blank'); }, [cart, appUrl]); const baseReturn = { subscriptionPlans, modelPortfolios, selectedCycles, planType, sortBy, loading, error, setSelectedCycle, setPlanType, setSortBy, processApiResponse, getDisplayedPlans, getDiscountInfo, getAvailableCycles: getAvailableCycles$1, getPlanStatistics, generateSinglePlanCheckoutUrl, formatPrice, getPlanPrice, }; // Add cart functionality only when enabled if (enableCart) { return { ...baseReturn, cart, cartItemCount, cartTotal, showCart, addToCart, removeFromCart, updateCartItemCycle, clearCart, setShowCart, handleCheckout, }; } return baseReturn; }; export { calculateCartTotal, calculateDiscountInfo, calculateSavings, createCheckoutPayload, createSubscriptionState, ensureInitialized, filterPlansBySearch, formatCurrency, formatPrice, generateBenefitsData, generateCheckoutUrl, generateFAQData, generateSinglePlanCheckoutUrl, getAvailableCycles, getBestDiscountPercentage, getCycleSelectionClasses, getDefaultCycles, getDisplayPlans, getDisplayedPlans, getIcon, getIconColor, getMinimumPriceCycle, getPackageStatus, getPeriodLabel, getPlanStatistics, getSubscriptionData, handleBuyNow, handleKeyDown, handlePeriodSelect, hasDiscounts, isSelected, parseFeatures, parsePlan, processSubscriptionData, scrollToSection, setApiKey, sortPlans, transformApiResponse, updateSelectedCycles, useSubscriptionData, validateApiResponse, validatePlan }; //# sourceMappingURL=index.esm.js.map