import { Button } from '@/components/ui/button'; import { Dialog, DialogClose, DialogContent, DialogDescription, DialogFooter, DialogTitle, } from '@/components/ui/dialog'; import { Slider } from '@/components/ui/slider'; import { Switch } from '@/components/ui/switch'; import { Tooltiper } from '@/components/ui/tooltip'; import { Widget, WidgetBody, WidgetHead } from '@/components/widget'; import { useAppParams } from '@/hooks/use-app-params'; import useWS from '@/hooks/use-ws'; import { useTRPC } from '@/integrations/trpc/react'; import { showConfirm } from '@/modals'; import { op } from '@/utils/op'; import type { IServiceOrganization } from '@openpanel/db'; import type { IPolarPrice } from '@openpanel/payments'; import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; import { Loader2Icon } from 'lucide-react'; import { useQueryState } from 'nuqs'; import { useEffect, useMemo, useState } from 'react'; import { toast } from 'sonner'; type Props = { organization: IServiceOrganization; }; export default function Billing({ organization }: Props) { const { projectId } = useAppParams(); const queryClient = useQueryClient(); const trpc = useTRPC(); const [customerSessionToken, setCustomerSessionToken] = useQueryState( 'customer_session_token', ); const productsQuery = useQuery( trpc.subscription.products.queryOptions({ organizationId: organization.id, }), ); useWS(`/live/organization/${organization.id}`, () => { queryClient.invalidateQueries(trpc.organization.pathFilter()); }); const [recurringInterval, setRecurringInterval] = useState<'year' | 'month'>( (organization.subscriptionInterval as 'year' | 'month') || 'month', ); const products = useMemo(() => { return (productsQuery.data || []) .filter((product) => product.recurringInterval === recurringInterval) .filter((product) => product.prices.some((p) => p.amountType !== 'free')); }, [productsQuery.data, recurringInterval]); useEffect(() => { if (organization.subscriptionInterval) { setRecurringInterval( organization.subscriptionInterval as 'year' | 'month', ); } }, [organization.subscriptionInterval]); useEffect(() => { if (customerSessionToken) { op.track('subscription_created'); } }, [customerSessionToken]); const [selectedProductIndex, setSelectedProductIndex] = useState(0); // Check if organization has a custom product const hasCustomProduct = useMemo(() => { return products.some((product) => product.metadata?.custom === true); }, [products]); // Find current subscription index const currentSubscriptionIndex = useMemo(() => { if (!organization.subscriptionProductId) { // Default to 100K events plan if no subscription const defaultIndex = products.findIndex( (product) => product.metadata?.eventsLimit === 100_000, ); return defaultIndex >= 0 ? defaultIndex : 0; } return products.findIndex( (product) => product.id === organization.subscriptionProductId, ); }, [products, organization.subscriptionProductId]); // Check if selected index is the "custom" option (beyond available products) const isCustomOption = selectedProductIndex >= products.length; // Find the highest event limit to make the custom option dynamic const highestEventLimit = useMemo(() => { const limits = products .map((product) => product.metadata?.eventsLimit) .filter((limit): limit is number => typeof limit === 'number'); return Math.max(...limits, 0); }, [products]); // Format the custom option label dynamically const customOptionLabel = useMemo(() => { if (highestEventLimit >= 1_000_000) { return `+${(highestEventLimit / 1_000_000).toFixed(0)}M`; } if (highestEventLimit >= 1_000) { return `+${(highestEventLimit / 1_000).toFixed(0)}K`; } return `+${highestEventLimit}`; }, [highestEventLimit]); // Set initial slider position to current subscription useEffect(() => { if (currentSubscriptionIndex >= 0) { setSelectedProductIndex(currentSubscriptionIndex); } }, [currentSubscriptionIndex]); const selectedProduct = products[selectedProductIndex]; const isUpgrade = selectedProductIndex > currentSubscriptionIndex; const isDowngrade = selectedProductIndex < currentSubscriptionIndex; const isCurrentPlan = selectedProductIndex === currentSubscriptionIndex; function renderBillingSlider() { if (productsQuery.isLoading) { return (
); } if (productsQuery.isError) { return (
Issues loading all tiers
); } if (hasCustomProduct) { return (
Not applicable since custom product
); } return (
Select your plan {selectedProduct?.name || 'No plan selected'}
setSelectedProductIndex(value)} min={0} max={products.length} // +1 for the custom option step={1} className="w-full" disabled={hasCustomProduct} />
{products.map((product, index) => { const eventsLimit = product.metadata?.eventsLimit; return (
{eventsLimit && typeof eventsLimit === 'number' ? `${(eventsLimit / 1000).toFixed(0)}K` : 'Free'}
events
); })} {/* Add the custom option label */}
{customOptionLabel}
events
{(selectedProduct || isCustomOption) && (
{isCustomOption ? ( // Custom option content <>

Custom Plan

{customOptionLabel} events per {recurringInterval}

Custom Pricing

Need higher limits?

Reach out to{' '} hello@openpanel.dev {' '} and we'll help you with a custom quota.

) : ( // Regular product content <>

{selectedProduct.name}

{selectedProduct.metadata?.eventsLimit ? `${selectedProduct.metadata.eventsLimit.toLocaleString()} events per ${recurringInterval}` : 'Free tier'}

{selectedProduct.prices[0]?.amountType === 'free' ? ( Free ) : ( {new Intl.NumberFormat('en-US', { style: 'currency', currency: selectedProduct.prices[0]?.priceCurrency || 'USD', minimumFractionDigits: 0, maximumFractionDigits: 1, }).format( (selectedProduct.prices[0] && 'priceAmount' in selectedProduct.prices[0] ? selectedProduct.prices[0].priceAmount : 0) / 100, )} {' / '} {recurringInterval === 'year' ? 'year' : 'month'} )}
{!isCurrentPlan && selectedProduct.prices[0] && (
)} {isCurrentPlan && (
)} )}
)}
); } return ( <> Billing
{recurringInterval === 'year' ? 'Yearly (2 months free)' : 'Monthly'} setRecurringInterval(checked ? 'year' : 'month') } />
{renderBillingSlider()}
{ setCustomerSessionToken(null); if (!open) { queryClient.invalidateQueries(trpc.organization.pathFilter()); } }} > Subscription created We have registered your subscription. It'll be activated within a couple of seconds. ); } function CheckoutButton({ price, organization, projectId, disabled, buttonText, }: { price: IPolarPrice; organization: IServiceOrganization; projectId: string; disabled?: string | null; buttonText?: string; }) { const trpc = useTRPC(); const isCurrentPrice = organization.subscriptionPriceId === price.id; const checkout = useMutation( trpc.subscription.checkout.mutationOptions({ onSuccess(data) { if (data?.url) { window.location.href = data.url; } else { toast.success('Subscription updated', { description: 'It might take a few seconds to update', }); } }, }), ); const isCanceled = organization.subscriptionStatus === 'active' && isCurrentPrice && organization.subscriptionCanceledAt; const isActive = organization.subscriptionStatus === 'active' && isCurrentPrice; return ( ); }