Merge pull request #86 from kortix-ai/remove-free-tier-for-self-hosting

remove free tier from cloud version for now
This commit is contained in:
Adam Cohen Hillel 2025-04-23 19:29:46 +01:00 committed by GitHub
commit 7bffa72056
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 146 additions and 84 deletions

View File

@ -3,7 +3,7 @@ from typing import Dict, Optional, Tuple
# Define subscription tiers and their monthly limits (in minutes)
SUBSCRIPTION_TIERS = {
'price_1RGJ9GG6l1KZGqIroxSqgphC': {'name': 'free', 'minutes': 10},
'price_1RGJ9GG6l1KZGqIroxSqgphC': {'name': 'free', 'minutes': 0},
'price_1RGJ9LG6l1KZGqIrd9pwzeNW': {'name': 'base', 'minutes': 300}, # 100 hours = 6000 minutes
'price_1RGJ9JG6l1KZGqIrVUU4ZRv6': {'name': 'extra', 'minutes': 2400} # 100 hours = 6000 minutes
}
@ -76,11 +76,14 @@ async def check_billing_status(client, account_id: str) -> Tuple[bool, str, Opti
subscription = await get_account_subscription(client, account_id)
# If no subscription, they can use free tier
if not subscription:
subscription = {
'price_id': 'price_1RGJ9GG6l1KZGqIroxSqgphC', # Free tier
'plan_name': 'Free'
}
# if not subscription:
# subscription = {
# 'price_id': 'price_1RGJ9GG6l1KZGqIroxSqgphC', # Free tier
# 'plan_name': 'Free'
# }
if not subscription or subscription.get('price_id') is None or subscription.get('price_id') == 'price_1RGJ9GG6l1KZGqIroxSqgphC':
return False, "You are not subscribed to any plan. Please upgrade your plan to continue.", subscription
# Get tier info
tier_info = SUBSCRIPTION_TIERS.get(subscription['price_id'])

View File

@ -7,6 +7,7 @@ import {
SidebarProvider,
} from "@/components/ui/sidebar"
import { MaintenanceAlert } from "@/components/maintenance-alert"
import { useAccounts } from "@/hooks/use-accounts"
interface DashboardLayoutProps {
children: React.ReactNode
@ -16,6 +17,8 @@ export default function DashboardLayout({
children,
}: DashboardLayoutProps) {
const [showMaintenanceAlert, setShowMaintenanceAlert] = useState(false)
const { data: accounts } = useAccounts()
const personalAccount = accounts?.find(account => account.personal_account)
useEffect(() => {
// Show the maintenance alert when component mounts
@ -35,6 +38,7 @@ export default function DashboardLayout({
open={showMaintenanceAlert}
onOpenChange={setShowMaintenanceAlert}
closeable={true}
accountId={personalAccount?.account_id}
/>
</SidebarProvider>
)

View File

@ -1,94 +1,149 @@
"use client"
import { AlertDialog, AlertDialogAction, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle } from "@/components/ui/alert-dialog"
import { Clock, Github, X } from "lucide-react"
import { AlertDialog, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle } from "@/components/ui/alert-dialog"
import { AlertCircle, X, Zap, Github } from "lucide-react"
import Link from "next/link"
import { AnimatePresence, motion } from "motion/react"
import { Button } from "@/components/ui/button"
import { Portal } from "@/components/ui/portal"
import { cn } from "@/lib/utils"
import { setupNewSubscription } from "@/lib/actions/billing"
import { SubmitButton } from "@/components/ui/submit-button"
import { siteConfig } from "@/lib/home"
interface MaintenanceAlertProps {
open: boolean
onOpenChange: (open: boolean) => void
closeable?: boolean
accountId?: string | null | undefined
}
export function MaintenanceAlert({ open, onOpenChange, closeable = true }: MaintenanceAlertProps) {
export function MaintenanceAlert({ open, onOpenChange, closeable = true, accountId }: MaintenanceAlertProps) {
const returnUrl = typeof window !== 'undefined' ? window.location.href : '';
if (!open) return null;
// Filter plans to show only Pro and Enterprise
const premiumPlans = siteConfig.cloudPricingItems.filter(plan =>
plan.name === 'Pro' || plan.name === 'Enterprise'
);
return (
<AlertDialog open={open} onOpenChange={closeable ? onOpenChange : undefined}>
<AlertDialogContent className="max-w-2xl w-[90vw] p-0 border-0 shadow-lg overflow-hidden rounded-2xl z-[9999]">
<motion.div
className="relative"
initial={{ opacity: 0, scale: 0.95 }}
animate={{ opacity: 1, scale: 1 }}
transition={{ type: "spring", damping: 30, stiffness: 300 }}
>
{/* Background pattern */}
<div className="absolute inset-0 bg-accent/20 opacity-20">
<div className="absolute inset-0 bg-grid-white/10 [mask-image:radial-gradient(white,transparent_85%)]" />
</div>
{closeable && (
<Button
variant="ghost"
size="icon"
className="absolute right-4 top-4 z-20 rounded-full hover:bg-background/80"
onClick={() => onOpenChange(false)}
<Portal>
<AnimatePresence>
{open && (
<>
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
transition={{ duration: 0.2 }}
className="fixed inset-0 z-[9999] flex items-center justify-center overflow-y-auto py-4"
>
<X className="h-4 w-4" />
<span className="sr-only">Close</span>
</Button>
)}
<AlertDialogHeader className="gap-6 px-8 pt-10 pb-6 relative z-10">
<motion.div
className="flex items-center justify-center"
initial={{ y: -10, opacity: 0 }}
animate={{ y: 0, opacity: 1 }}
transition={{ delay: 0.1 }}
>
<div className="flex size-16 items-center justify-center rounded-full bg-gradient-to-t from-primary/20 to-secondary/10 backdrop-blur-md">
<div className="flex size-12 items-center justify-center rounded-full bg-gradient-to-t from-primary to-primary/80 shadow-md">
<Clock className="h-6 w-6 text-white" />
{/* Backdrop */}
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
transition={{ duration: 0.2 }}
className="fixed inset-0 bg-black/40 backdrop-blur-sm"
onClick={closeable ? () => onOpenChange(false) : undefined}
aria-hidden="true"
/>
{/* Modal */}
<motion.div
initial={{ opacity: 0, scale: 0.95, y: 20 }}
animate={{ opacity: 1, scale: 1, y: 0 }}
exit={{ opacity: 0, scale: 0.95, y: 20 }}
transition={{ duration: 0.2, ease: [0.4, 0, 0.2, 1] }}
className={cn(
"relative bg-background rounded-lg shadow-xl w-full max-w-md mx-3"
)}
role="dialog"
aria-modal="true"
aria-labelledby="free-tier-modal-title"
>
<div className="p-4">
{/* Close button */}
{closeable && (
<button
onClick={() => onOpenChange(false)}
className="absolute top-2 right-2 text-muted-foreground hover:text-foreground transition-colors"
aria-label="Close dialog"
>
<X className="h-4 w-4" />
</button>
)}
{/* Header */}
<div className="text-center mb-4">
<div className="inline-flex items-center justify-center p-1.5 bg-primary/10 rounded-full mb-2">
<Zap className="h-4 w-4 text-primary" />
</div>
<h2 id="free-tier-modal-title" className="text-lg font-medium tracking-tight mb-1">
Free Tier Unavailable At This Time
</h2>
<p className="text-xs text-muted-foreground">
Due to extremely high demand, we cannot offer a free tier at the moment. Upgrade to Pro to continue using our service.
</p>
</div>
{/* Custom plan comparison wrapper to show Pro, Enterprise and Self-Host side by side */}
<div className="grid grid-cols-3 gap-2 mb-3">
{premiumPlans.map((tier) => (
<div key={tier.name} className="border border-border rounded-lg p-3">
<div className="text-center mb-2">
<h3 className="font-medium">{tier.name}</h3>
<p className="text-sm font-bold">{tier.price}/mo</p>
<p className="text-xs text-muted-foreground">{tier.hours}/month</p>
</div>
<form>
<input type="hidden" name="accountId" value={accountId || ''} />
<input type="hidden" name="returnUrl" value={returnUrl} />
<input type="hidden" name="planId" value={
tier.name === 'Pro'
? siteConfig.cloudPricingItems.find(item => item.name === 'Pro')?.stripePriceId || ''
: siteConfig.cloudPricingItems.find(item => item.name === 'Enterprise')?.stripePriceId || ''
} />
<SubmitButton
pendingText="..."
formAction={setupNewSubscription}
className={cn(
"w-full font-medium transition-colors h-7 rounded-md text-xs",
tier.buttonColor
)}
>
Upgrade
</SubmitButton>
</form>
</div>
))}
{/* Self-host Option as the third card */}
<div className="border border-border rounded-lg p-3">
<div className="text-center mb-2">
<h3 className="font-medium">Self-Host</h3>
<p className="text-sm font-bold">Free</p>
<p className="text-xs text-muted-foreground">Open Source</p>
</div>
<Link
href="https://github.com/kortix-ai/suna"
target="_blank"
rel="noopener noreferrer"
className="w-full flex items-center justify-center gap-1 h-7 bg-gradient-to-tr from-primary to-primary/80 hover:opacity-90 text-white font-medium rounded-md text-xs transition-all"
>
<Github className="h-3.5 w-3.5" />
<span>Self-Host</span>
</Link>
</div>
</div>
</div>
</div>
</motion.div>
</motion.div>
<motion.div
initial={{ y: -10, opacity: 0 }}
animate={{ y: 0, opacity: 1 }}
transition={{ delay: 0.2 }}
>
<AlertDialogTitle className="text-2xl font-bold text-center text-primary bg-clip-text">
High Demand Notice
</AlertDialogTitle>
</motion.div>
<motion.div
initial={{ y: -10, opacity: 0 }}
animate={{ y: 0, opacity: 1 }}
transition={{ delay: 0.3 }}
>
<AlertDialogDescription className="text-base text-center leading-relaxed">
Due to exceptionally high demand, our service is currently experiencing slower response times.
We recommend returning tomorrow when our systems will be operating at normal capacity.
<span className="mt-4 block font-medium text-primary">Thank you for your understanding. We will notify you via email once the service is fully operational again.</span>
</AlertDialogDescription>
</motion.div>
</AlertDialogHeader>
<AlertDialogFooter className="p-8 pt-4 border-t border-border/40 bg-background/40 backdrop-blur-sm">
<Link
href="https://github.com/kortix-ai/suna"
target="_blank"
rel="noopener noreferrer"
className="mx-auto w-full flex items-center justify-center gap-3 bg-gradient-to-tr from-primary to-primary/80 hover:opacity-90 text-white font-medium rounded-full px-8 py-3 transition-all hover:shadow-md"
>
<Github className="h-5 w-5 transition-transform group-hover:scale-110" />
<span>Explore Self-Hosted Version</span>
</Link>
</AlertDialogFooter>
</motion.div>
</AlertDialogContent>
</AlertDialog>
</>
)}
</AnimatePresence>
</Portal>
)
}

View File

@ -83,9 +83,9 @@ export const siteConfig = {
buttonText: "Hire Suna",
buttonColor: "bg-secondary text-white",
isPopular: false,
hours: "10 minutes",
hours: "no free usage at this time",
features: [
"10 minutes usage per month",
"no free usage",
// "Community support",
// "Single user",
// "Standard response time",