import React, { useState, useEffect, useContext, useRef } from "react"; import ReactGA from "react-ga4"; import { getTheme } from "../theme.jsx"; import { Box, Typography, Divider, Button, Grid, LinearProgress, Dialog, DialogTitle, DialogContent, TextField, InputAdornment, IconButton, Chip, Checkbox, Tooltip, DialogActions, FormControlLabel, Switch, CircularProgress, Skeleton, } from "@mui/material"; import { useNavigate, Link } from "react-router-dom"; import { toast } from "react-toastify"; import { Context } from "../context/ContextApi.jsx"; import { ContentCopy as ContentCopyIcon, Draw as DrawIcon, Close as CloseIcon, Done as DoneIcon, } from "@mui/icons-material"; // This is the main component which shows the cards on Billing & Stats tab const LicencePopup = (props) => { const { globalUrl, userdata, serverside, billingInfo, stripeKey, setModalOpen, isScale, isLoggedIn, isMobile, monthlyAppRunsParent, monthlyAllSuborgExecutions, selectedOrganization, setSelectedOrganization, isCloud, features, handleGetOrg, licensePopup = false, } = props; //const alert = useAlert(); let navigate = useNavigate(); const [shuffleVariant, setShuffleVariant] = useState(isCloud ? 0 : 1); const [BillingEmail, setBillingEmail] = useState( selectedOrganization?.Billing?.Email ); const { themeMode } = useContext(Context); const theme = getTheme(themeMode); useEffect(() => { if (selectedOrganization?.Billing?.Email !== BillingEmail) { setBillingEmail(selectedOrganization?.Billing?.Email); } }, [selectedOrganization]); const [billingCycle, setBillingCycle] = useState("annual"); const [scaleValue, setScaleValue] = useState( new URLSearchParams(window.location.search).get("app_runs") || userdata?.app_execution_limit / 1000 + 50 || 10 ); const [isLoading, setIsLoading] = useState(true); useEffect(() => { setScaleValue(userdata?.app_execution_limit / 1000 + 50 || 10); }, [userdata]); // Set loading state based on data availability useEffect(() => { if (selectedOrganization && userdata) { // Add a small delay to show skeleton briefly for better UX const timer = setTimeout(() => { setIsLoading(false); }, 500); return () => clearTimeout(timer); } }, [selectedOrganization, userdata]); const getPrice = (basePrice) => { return Math.round(billingCycle === "annual" ? basePrice * 0.9 : basePrice); // 10% discount for annual }; const stripe = typeof window === "undefined" || window.location === undefined ? "" : props.stripeKey === undefined ? "" : window.Stripe ? window.Stripe(props.stripeKey) : ""; // Handle slider change for Scale plan const handleScaleChange = (event, newValue) => { setScaleValue(newValue); // Add app runs to URL query params const urlSearchParams = new URLSearchParams(window.location.search); urlSearchParams.set("app_runs", newValue); // Convert to actual app runs (k to actual number) const newUrl = `${window.location.pathname}?${urlSearchParams.toString()}`; window.history.replaceState({}, "", newUrl); }; // Handle billing cycle change const handleBillingCycleChange = (event, newValue) => { if (newValue !== null) { setBillingCycle(newValue); if (isCloud) { ReactGA.event({ category: "Billingpage", action: "Billing Cycle Changed", label: `${billingCycle} -> ${newValue}`, }); } // Add billing cycle to URL query params const urlSearchParams = new URLSearchParams(window.location.search); urlSearchParams.set("billing_cycle", newValue); const newUrl = `${ window.location.pathname }?${urlSearchParams.toString()}`; window.history.replaceState({}, "", newUrl); } }; const payasyougo = "Pay as you go"; const paperStyle = { padding: 20, paddingBottom: 30, borderRadius: theme.palette?.borderRadius, height: "100%", }; const userInScalePlan = userdata?.app_execution_limit > 2000; // These functions are being used for the dynamic features from the orgSyncFeatures // Add this function to format the limit value const formatLimit = (limit) => { if (limit === null || limit === undefined || limit === 0) return "Unlimited"; if (typeof limit === "string" && limit.toLowerCase() === "unlimited") return "Unlimited"; if (typeof limit === "number") return limit.toLocaleString(); return limit.toString(); }; // Add this function to format the feature text with proper unlimited handling const formatFeatureText = (feature, limit) => { if (!feature) return ""; // Dynamic features that use limits const featureMapping = { app_executions: (limit) => { const formattedLimit = formatLimit(limit); return `Includes ${formattedLimit} App Executions per month`; }, multi_env: (limit) => { const formattedLimit = formatLimit(limit); return formattedLimit === "Unlimited" ? "Unlimited Environments" : `${formattedLimit} Environment${ parseInt(formattedLimit) > 1 ? "s" : "" }`; }, multi_tenant: (limit) => { const formattedLimit = formatLimit(limit); return formattedLimit === "Unlimited" ? "Unlimited Tenants" : `${formattedLimit} Tenant${ parseInt(formattedLimit) > 1 ? "s" : "" }`; }, multi_region: (limit) => { const formattedLimit = formatLimit(limit); return formattedLimit === "Unlimited" ? "Unlimited Regions" : `${formattedLimit} Region${ parseInt(formattedLimit) > 1 ? "s" : "" }`; }, webhook: (limit) => { const formattedLimit = formatLimit(limit); return formattedLimit === "Unlimited" ? "Unlimited Webhooks" : `${formattedLimit} Webhook${ parseInt(formattedLimit) > 1 ? "s" : "" }`; }, schedules: (limit) => { const formattedLimit = formatLimit(limit); return formattedLimit === "Unlimited" ? "Unlimited Schedules" : `${formattedLimit} Schedule${ parseInt(formattedLimit) > 1 ? "s" : "" }`; }, user_input: (limit) => { const formattedLimit = formatLimit(limit); return formattedLimit === "Unlimited" ? "Unlimited User Inputs" : `${formattedLimit} User Input${ parseInt(formattedLimit) > 1 ? "s" : "" }`; }, send_mail: (limit) => { const formattedLimit = formatLimit(limit); return formattedLimit === "Unlimited" ? "Unlimited Emails per month" : `${formattedLimit} Email${ parseInt(formattedLimit) > 1 ? "s" : "" } per month`; }, send_sms: (limit) => { const formattedLimit = formatLimit(limit); return formattedLimit === "Unlimited" ? "Unlimited SMS per month" : `${formattedLimit} SMS${ parseInt(formattedLimit) > 1 ? "" : "" } per month`; }, email_trigger: (limit) => { const formattedLimit = formatLimit(limit); return formattedLimit === "Unlimited" ? "Unlimited Email Triggers" : `${formattedLimit} Email Trigger${ parseInt(formattedLimit) > 1 ? "s" : "" }`; }, notifications: (limit) => { const formattedLimit = formatLimit(limit); return formattedLimit === "Unlimited" ? "Unlimited Notifications" : `${formattedLimit} Notification${ parseInt(formattedLimit) > 1 ? "s" : "" }`; }, workflows: (limit) => { const formattedLimit = formatLimit(limit); return formattedLimit === "Unlimited" ? "Unlimited Workflows" : `${formattedLimit} Workflow${ parseInt(formattedLimit) > 1 ? "s" : "" }`; }, autocomplete: (limit) => { const formattedLimit = formatLimit(limit); return formattedLimit === "Unlimited" ? "Unlimited Autocomplete" : `${formattedLimit} Autocomplete${ parseInt(formattedLimit) > 1 ? "s" : "" }`; }, workflow_executions: (limit) => { const formattedLimit = formatLimit(limit); return formattedLimit === "Unlimited" ? "Unlimited Workflow Executions" : `${formattedLimit} Workflow Execution${ parseInt(formattedLimit) > 1 ? "s" : "" }`; }, authentication: (limit) => { const formattedLimit = formatLimit(limit); return formattedLimit === "Unlimited" ? "Unlimited Authentication" : `${formattedLimit} Authentication${ parseInt(formattedLimit) > 1 ? "s" : "" }`; }, shuffle_gpt: (limit) => { const formattedLimit = formatLimit(limit); return formattedLimit === "Unlimited" ? "Unlimited Shuffle GPT" : `${formattedLimit} Shuffle GPT${ parseInt(formattedLimit) > 1 ? "s" : "" }`; }, branding: (limit) => { const formattedLimit = formatLimit(limit); return formattedLimit === "Unlimited" ? "Unlimited Branding" : `${formattedLimit} Branding${ parseInt(formattedLimit) > 1 ? "s" : "" }`; } }; try { // Check if we have a mapping for this feature const formatter = featureMapping[feature]; if (formatter) { return formatter(limit); } // Default format for unknown features const formattedLimit = formatLimit(limit); return `${feature}: ${formattedLimit}`; } catch (error) { console.warn(`Error formatting feature ${feature}:`, error); return `${feature}: ${formatLimit(limit)}`; } }; // Send signature request to backend const sendSignatureRequest = (subscription) => { const url = `${globalUrl}/api/v1/orgs/${selectedOrganization.id}`; fetch(url, { body: JSON.stringify({ org_id: selectedOrganization.id, editing: "subscription_update", subscription_index: 0, subscription: subscription, }), mode: "cors", method: "POST", credentials: "include", crossDomain: true, withCredentials: true, headers: { "Content-Type": "application/json; charset=utf-8", }, }) .then((response) => { if (response.status !== 200) { console.log("Error in response"); } return response.json(); }) .then((responseJson) => { console.log("Response from signature request: ", responseJson); if (typeof handleGetOrg === "function") { handleGetOrg(selectedOrganization.id); } }) .catch((error) => { console.log("Error: ", error); }); }; // Create a function to remove duplicates and merge features const mergeUniqueFeatures = (existingFeatures, newFeatures) => { // Convert arrays to Sets to remove duplicates const uniqueFeatures = new Set([ ...(existingFeatures || []), ...(newFeatures || []), ]); return Array.from(uniqueFeatures); }; // This is the dialog for editing subscription with better UX const EditSubscriptionDialog = ({ open, onClose, subscription, globalUrl, selectedOrganization, onSaved, }) => { const initialForm = { name: subscription?.name || "", active: !!subscription?.active, support_level: subscription?.support_level || "", recurrence: subscription?.recurrence || "month", amount: subscription?.amount || "", currency: subscription?.currency || "USD", level: subscription?.level || "", limit: subscription?.limit || 0, startdate: subscription?.startdate || 0, enddate: subscription?.enddate || 0, cancellationdate: subscription?.cancellationdate || 0, features: Array.isArray(subscription?.features) ? subscription.features : [], eula: subscription?.eula, eula_signed: subscription?.eula_signed, eula_signed_by: subscription?.eula_signed_by, reference: subscription?.reference, }; const [form, setForm] = useState(initialForm); useEffect(() => { if (!open) return; setForm(initialForm); setFeaturesMarkdown(featuresToMarkdown(subscription?.features)); setErrors({}); }, [open, subscription]); const handleCancel = () => { setForm(initialForm); setFeaturesMarkdown(featuresToMarkdown(subscription?.features)); setErrors({}); onClose?.(); }; const toInputDate = (epoch) => { if (!epoch || isNaN(epoch)) return ""; try { return new Date(epoch * 1000).toISOString().slice(0, 10); } catch (e) { return ""; } }; const toEpoch = (dateStr) => { if (!dateStr) return 0; const ms = Date.parse(dateStr); return isNaN(ms) ? 0 : Math.floor(ms / 1000); }; const [errors, setErrors] = useState({}); const [saving, setSaving] = useState(false); // Simple helper: Convert features array <-> markdown list const featuresToMarkdown = (arr) => { const list = Array.isArray(arr) ? arr : []; return list .map((line) => { const text = String(line || ""); // If already looks like a list item, keep as-is if (/^\s*-\s+/.test(text)) return text; return `- ${text}`; }) .join("\n"); }; const markdownToFeatures = (markdown) => { if (!markdown) return []; return String(markdown) .split("\n") .map((raw) => raw.replace(/\s+$/, "")) .filter(Boolean) .map((line) => { // Keep indentation depth of multiples of two spaces before dash const m = line.match(/^(\s*)-\s+(.*)$/); if (!m) { return line.trim(); } const indent = m[1] || ""; const text = m[2] || ""; return `${indent}- ${text}`.trimEnd(); }); }; const [featuresMarkdown, setFeaturesMarkdown] = useState( featuresToMarkdown(subscription?.features) ); const featuresInputRef = useRef(null); // Handle tab indentation for markdown textarea const handleFeaturesKeyDown = (e) => { if (e.key !== "Tab") return; const textarea = featuresInputRef.current; if (!textarea) return; e.preventDefault(); const { selectionStart, selectionEnd } = textarea; const text = featuresMarkdown; // Find the start and end of the current line(s) const lineStart = text.lastIndexOf("\n", selectionStart - 1) + 1; const lineEnd = text.indexOf("\n", selectionEnd); const actualLineEnd = lineEnd === -1 ? text.length : lineEnd; // Get the selected lines const selectedText = text.slice(lineStart, actualLineEnd); const lines = selectedText.split("\n"); // Apply indentation const indent = " "; // 2 spaces const newLines = lines.map(line => { if (e.shiftKey) { // Shift+Tab: remove indentation if (line.startsWith(indent)) { return line.slice(indent.length); } if (line.startsWith(" ")) { return line.slice(1); } return line; } else { // Tab: add indentation return `${indent}${line}`; } }); // Update the text const newText = text.slice(0, lineStart) + newLines.join("\n") + text.slice(actualLineEnd); setFeaturesMarkdown(newText); // Update cursor position const indentChange = e.shiftKey ? -indent.length : indent.length; const newSelectionStart = Math.max(lineStart, selectionStart + indentChange); const newSelectionEnd = Math.max(lineStart, selectionEnd + (indentChange * lines.length)); // Use requestAnimationFrame for better performance than setTimeout requestAnimationFrame(() => { try { textarea.selectionStart = newSelectionStart; textarea.selectionEnd = newSelectionEnd; } catch (error) { // Ignore selection errors } }); }; const validate = () => { const next = {}; if (!form.name || form.name.trim().length === 0) next.name = "Name is required"; if (form.amount !== "" && Number.isNaN(Number(form.amount))) next.amount = "Amount must be a number"; if (form.limit !== "" && Number.isNaN(Number(form.limit))) next.limit = "Limit must be a number"; if (form.startdate && form.enddate && form.enddate < form.startdate) next.enddate = "End date must be after start date"; if (!form.recurrence || String(form.recurrence).trim().length === 0) next.recurrence = "Recurrence is required"; setErrors(next); return Object.keys(next).length === 0; }; const save = async () => { if (!validate()) return; setSaving(true); try { const url = `${globalUrl}/api/v1/orgs/${selectedOrganization.id}`; const payload = { org_id: selectedOrganization.id, editing: "subscription_update", subscription_index: 0, subscription: { ...form, // Ensure backend gets array of features features: markdownToFeatures(featuresMarkdown), }, }; const res = await fetch(url, { method: "POST", credentials: "include", headers: { "Content-Type": "application/json" }, body: JSON.stringify(payload), }); const data = await res.json().catch(() => ({})); if (res.ok && data && data.success !== false) { toast.success("Subscription updated"); onClose?.(); onSaved?.({ ...form, features: markdownToFeatures(featuresMarkdown), }); } else { toast.error("Failed to update subscription"); } } catch (e) { toast.error("Failed to update subscription"); } finally { setSaving(false); } }; return ( ); }; // Skeleton loading component for subscription cards const SubscriptionSkeleton = () => (