mirror of https://github.com/kortix-ai/suna.git
billing popup
This commit is contained in:
parent
838070519a
commit
8751a1a716
|
@ -11,6 +11,9 @@ import { useIsMobile } from "@/hooks/use-mobile";
|
|||
import { useSidebar } from "@/components/ui/sidebar";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
|
||||
import { useBillingError } from "@/hooks/useBillingError";
|
||||
import { BillingErrorAlert } from "@/components/billing/BillingErrorAlert";
|
||||
import { useAccounts } from "@/hooks/use-accounts";
|
||||
|
||||
// Constant for localStorage key to ensure consistency
|
||||
const PENDING_PROMPT_KEY = 'pendingAgentPrompt';
|
||||
|
@ -19,9 +22,12 @@ function DashboardContent() {
|
|||
const [inputValue, setInputValue] = useState("");
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
const [autoSubmit, setAutoSubmit] = useState(false);
|
||||
const { billingError, handleBillingError, clearBillingError } = useBillingError();
|
||||
const router = useRouter();
|
||||
const isMobile = useIsMobile();
|
||||
const { setOpenMobile } = useSidebar();
|
||||
const { data: accounts } = useAccounts();
|
||||
const personalAccount = accounts?.find(account => account.personal_account);
|
||||
|
||||
const handleSubmit = async (message: string, options?: { model_name?: string; enable_thinking?: boolean }) => {
|
||||
if (!message.trim() || isSubmitting) return;
|
||||
|
@ -44,18 +50,102 @@ function DashboardContent() {
|
|||
// 3. Add the user message to the thread
|
||||
await addUserMessage(thread.thread_id, message.trim());
|
||||
|
||||
// 4. Start the agent with the thread ID
|
||||
const agentRun = await startAgent(thread.thread_id, {
|
||||
model_name: options?.model_name,
|
||||
enable_thinking: options?.enable_thinking,
|
||||
stream: true
|
||||
});
|
||||
try {
|
||||
// 4. Start the agent with the thread ID
|
||||
const agentRun = await startAgent(thread.thread_id, {
|
||||
model_name: options?.model_name,
|
||||
enable_thinking: options?.enable_thinking,
|
||||
stream: true
|
||||
});
|
||||
|
||||
// If successful, clear the pending prompt
|
||||
localStorage.removeItem(PENDING_PROMPT_KEY);
|
||||
// If successful, clear the pending prompt
|
||||
localStorage.removeItem(PENDING_PROMPT_KEY);
|
||||
|
||||
// 5. Navigate to the new agent's thread page
|
||||
router.push(`/agents/${thread.thread_id}`);
|
||||
// 5. Navigate to the new agent's thread page
|
||||
router.push(`/agents/${thread.thread_id}`);
|
||||
} catch (error: any) {
|
||||
// Check specifically for billing errors (402 Payment Required)
|
||||
if (error.message?.includes('(402)') || error?.status === 402) {
|
||||
console.log("Billing error detected:", error);
|
||||
|
||||
// Try to extract the error details from the error object
|
||||
try {
|
||||
// Try to parse the error.response or the error itself
|
||||
let errorDetails;
|
||||
|
||||
// First attempt: check if error.data exists and has a detail property
|
||||
if (error.data?.detail) {
|
||||
errorDetails = error.data.detail;
|
||||
console.log("Extracted billing error details from error.data.detail:", errorDetails);
|
||||
}
|
||||
// Second attempt: check if error.detail exists directly
|
||||
else if (error.detail) {
|
||||
errorDetails = error.detail;
|
||||
console.log("Extracted billing error details from error.detail:", errorDetails);
|
||||
}
|
||||
// Third attempt: try to parse the error text if it's JSON
|
||||
else if (typeof error.text === 'function') {
|
||||
const text = await error.text();
|
||||
console.log("Extracted error text:", text);
|
||||
try {
|
||||
const parsed = JSON.parse(text);
|
||||
errorDetails = parsed.detail || parsed;
|
||||
console.log("Parsed error text as JSON:", errorDetails);
|
||||
} catch (e) {
|
||||
// Not JSON, use regex to extract info
|
||||
console.log("Error text is not valid JSON");
|
||||
}
|
||||
}
|
||||
|
||||
// If we still don't have details, try to extract from the error message
|
||||
if (!errorDetails && error.message) {
|
||||
const match = error.message.match(/Monthly limit of (\d+) minutes reached/);
|
||||
if (match) {
|
||||
const minutes = parseInt(match[1]);
|
||||
errorDetails = {
|
||||
message: error.message,
|
||||
subscription: {
|
||||
price_id: "price_1RGJ9GG6l1KZGqIroxSqgphC", // Free tier by default
|
||||
plan_name: "Free",
|
||||
current_usage: minutes / 60, // Convert to hours
|
||||
limit: minutes / 60 // Convert to hours
|
||||
}
|
||||
};
|
||||
console.log("Extracted billing error details from error message:", errorDetails);
|
||||
}
|
||||
}
|
||||
|
||||
// Handle the billing error with the details we extracted
|
||||
if (errorDetails) {
|
||||
console.log("Handling billing error with extracted details:", errorDetails);
|
||||
handleBillingError(errorDetails);
|
||||
} else {
|
||||
// Fallback with generic billing error
|
||||
console.log("Using fallback generic billing error");
|
||||
handleBillingError({
|
||||
message: "You've reached your monthly usage limit. Please upgrade your plan.",
|
||||
subscription: {
|
||||
price_id: "price_1RGJ9GG6l1KZGqIroxSqgphC", // Free tier
|
||||
plan_name: "Free"
|
||||
}
|
||||
});
|
||||
}
|
||||
} catch (parseError) {
|
||||
console.error("Error parsing billing error details:", parseError);
|
||||
// Fallback with generic error
|
||||
handleBillingError({
|
||||
message: "You've reached your monthly usage limit. Please upgrade your plan."
|
||||
});
|
||||
}
|
||||
|
||||
// Don't rethrow - we've handled this error with the billing alert
|
||||
setIsSubmitting(false);
|
||||
return; // Exit handleSubmit
|
||||
}
|
||||
|
||||
// Rethrow any non-billing errors
|
||||
throw error;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error creating agent:", error);
|
||||
setIsSubmitting(false);
|
||||
|
@ -125,6 +215,16 @@ function DashboardContent() {
|
|||
hideAttachments={true}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Billing Error Alert */}
|
||||
<BillingErrorAlert
|
||||
message={billingError?.message}
|
||||
currentUsage={billingError?.currentUsage || billingError?.subscription?.current_usage}
|
||||
limit={billingError?.limit || billingError?.subscription?.limit}
|
||||
accountId={personalAccount?.account_id}
|
||||
onDismiss={clearBillingError}
|
||||
isOpen={!!billingError}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -38,7 +38,7 @@ export function BillingErrorAlert({
|
|||
animate={{ opacity: 1 }}
|
||||
exit={{ opacity: 0 }}
|
||||
transition={{ duration: 0.2 }}
|
||||
className="fixed inset-0 z-[9999] flex items-center justify-center"
|
||||
className="fixed inset-0 z-[9999] flex items-center justify-center overflow-y-auto py-4"
|
||||
>
|
||||
{/* Backdrop */}
|
||||
<motion.div
|
||||
|
@ -58,52 +58,52 @@ export function BillingErrorAlert({
|
|||
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-xl shadow-2xl w-full max-w-3xl mx-4",
|
||||
"relative bg-background rounded-lg shadow-xl w-full max-w-sm mx-3",
|
||||
className
|
||||
)}
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
aria-labelledby="billing-modal-title"
|
||||
>
|
||||
<div className="p-6">
|
||||
<div className="p-4">
|
||||
{/* Close button */}
|
||||
{onDismiss && (
|
||||
<button
|
||||
onClick={onDismiss}
|
||||
className="absolute top-4 right-4 text-muted-foreground hover:text-foreground transition-colors"
|
||||
className="absolute top-2 right-2 text-muted-foreground hover:text-foreground transition-colors"
|
||||
aria-label="Close dialog"
|
||||
>
|
||||
<X className="h-5 w-5" />
|
||||
<X className="h-4 w-4" />
|
||||
</button>
|
||||
)}
|
||||
|
||||
{/* Header */}
|
||||
<div className="text-center mb-8">
|
||||
<div className="inline-flex items-center justify-center p-2 bg-destructive/10 rounded-full mb-4">
|
||||
<AlertCircle className="h-6 w-6 text-destructive" />
|
||||
<div className="text-center mb-4">
|
||||
<div className="inline-flex items-center justify-center p-1.5 bg-destructive/10 rounded-full mb-2">
|
||||
<AlertCircle className="h-4 w-4 text-destructive" />
|
||||
</div>
|
||||
<h2 id="billing-modal-title" className="text-2xl font-medium tracking-tight mb-2">
|
||||
<h2 id="billing-modal-title" className="text-lg font-medium tracking-tight mb-1">
|
||||
Usage Limit Reached
|
||||
</h2>
|
||||
<p className="text-muted-foreground">
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{message || "You've reached your monthly usage limit."}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Usage Stats */}
|
||||
{currentUsage !== undefined && limit !== undefined && (
|
||||
<div className="mb-8 p-6 bg-[#F3F4F6] dark:bg-[#F9FAFB]/[0.02] border border-border rounded-xl">
|
||||
<div className="flex justify-between items-center mb-4">
|
||||
<div className="mb-4 p-3 bg-muted/30 border border-border rounded-lg">
|
||||
<div className="flex justify-between items-center mb-2">
|
||||
<div>
|
||||
<p className="text-sm font-medium text-muted-foreground">Current Usage</p>
|
||||
<p className="text-2xl font-semibold">{currentUsage} hours</p>
|
||||
<p className="text-xs font-medium text-muted-foreground">Usage</p>
|
||||
<p className="text-base font-semibold">{currentUsage}h</p>
|
||||
</div>
|
||||
<div className="text-right">
|
||||
<p className="text-sm font-medium text-muted-foreground">Monthly Limit</p>
|
||||
<p className="text-2xl font-semibold">{limit} hours</p>
|
||||
<p className="text-xs font-medium text-muted-foreground">Limit</p>
|
||||
<p className="text-base font-semibold">{limit}h</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="w-full h-2 bg-background rounded-full overflow-hidden">
|
||||
<div className="w-full h-1.5 bg-background rounded-full overflow-hidden">
|
||||
<motion.div
|
||||
initial={{ width: 0 }}
|
||||
animate={{ width: `${Math.min((currentUsage / limit) * 100, 100)}%` }}
|
||||
|
@ -118,14 +118,16 @@ export function BillingErrorAlert({
|
|||
<PlanComparison
|
||||
accountId={accountId}
|
||||
returnUrl={returnUrl}
|
||||
className="mb-6"
|
||||
className="mb-3"
|
||||
isCompact={true}
|
||||
/>
|
||||
|
||||
{/* Dismiss Button */}
|
||||
{onDismiss && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
className="w-full text-muted-foreground hover:text-foreground"
|
||||
size="sm"
|
||||
className="w-full text-muted-foreground hover:text-foreground text-xs h-7"
|
||||
onClick={onDismiss}
|
||||
>
|
||||
Continue with Current Plan
|
||||
|
|
|
@ -22,14 +22,15 @@ interface PlanComparisonProps {
|
|||
isManaged?: boolean;
|
||||
onPlanSelect?: (planId: string) => void;
|
||||
className?: string;
|
||||
isCompact?: boolean; // When true, uses vertical stacked layout for modals
|
||||
}
|
||||
|
||||
// Price display animation component
|
||||
const PriceDisplay = ({ tier }: { tier: typeof siteConfig.cloudPricingItems[number] }) => {
|
||||
const PriceDisplay = ({ tier, isCompact }: { tier: typeof siteConfig.cloudPricingItems[number]; isCompact?: boolean }) => {
|
||||
return (
|
||||
<motion.span
|
||||
key={tier.price}
|
||||
className="text-4xl font-semibold"
|
||||
className={isCompact ? "text-xl font-semibold" : "text-3xl font-semibold"}
|
||||
initial={{
|
||||
opacity: 0,
|
||||
x: 10,
|
||||
|
@ -48,7 +49,8 @@ export function PlanComparison({
|
|||
returnUrl = typeof window !== 'undefined' ? window.location.href : '',
|
||||
isManaged = true,
|
||||
onPlanSelect,
|
||||
className = ""
|
||||
className = "",
|
||||
isCompact = false
|
||||
}: PlanComparisonProps) {
|
||||
const [currentPlanId, setCurrentPlanId] = useState<string | undefined>();
|
||||
|
||||
|
@ -74,7 +76,15 @@ export function PlanComparison({
|
|||
}, [accountId]);
|
||||
|
||||
return (
|
||||
<div className={cn("grid min-[650px]:grid-cols-2 min-[900px]:grid-cols-3 gap-4 w-full max-w-6xl mx-auto", className)}>
|
||||
<div
|
||||
className={cn(
|
||||
"grid gap-3 w-full mx-auto",
|
||||
isCompact
|
||||
? "grid-cols-1 max-w-md"
|
||||
: "grid-cols-1 md:grid-cols-3 max-w-6xl",
|
||||
className
|
||||
)}
|
||||
>
|
||||
{siteConfig.cloudPricingItems.map((tier) => {
|
||||
const isCurrentPlan = currentPlanId === SUBSCRIPTION_PLANS[tier.name.toUpperCase() as keyof typeof SUBSCRIPTION_PLANS];
|
||||
|
||||
|
@ -82,108 +92,157 @@ export function PlanComparison({
|
|||
<div
|
||||
key={tier.name}
|
||||
className={cn(
|
||||
"rounded-xl bg-background border border-border p-6 flex flex-col gap-6",
|
||||
isCurrentPlan && "ring-2 ring-primary"
|
||||
"rounded-lg bg-background border border-border",
|
||||
isCompact ? "p-3 text-sm" : "p-5",
|
||||
isCurrentPlan && (isCompact ? "ring-1 ring-primary" : "ring-2 ring-primary")
|
||||
)}
|
||||
>
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<h3 className="text-lg font-medium">{tier.name}</h3>
|
||||
{tier.isPopular && (
|
||||
<span className="bg-primary text-primary-foreground text-xs font-medium px-2 py-0.5 rounded-full">
|
||||
Popular
|
||||
</span>
|
||||
)}
|
||||
{isCurrentPlan && (
|
||||
<span className="bg-secondary text-secondary-foreground text-xs font-medium px-2 py-0.5 rounded-full">
|
||||
Current Plan
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex items-baseline">
|
||||
<PriceDisplay tier={tier} />
|
||||
<span className="text-muted-foreground ml-2">
|
||||
{tier.price !== "$0" ? "/month" : ""}
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-muted-foreground">{tier.description}</p>
|
||||
<div className="inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-medium bg-secondary/10 text-secondary">
|
||||
{tier.hours}/month
|
||||
</div>
|
||||
</div>
|
||||
{isCompact ? (
|
||||
// Compact layout for modal
|
||||
<>
|
||||
<div className="flex justify-between mb-2">
|
||||
<div>
|
||||
<div className="flex items-center gap-1">
|
||||
<h3 className="font-medium">{tier.name}</h3>
|
||||
{tier.isPopular && (
|
||||
<span className="bg-primary/10 text-primary text-[10px] font-medium px-1.5 py-0.5 rounded-full">
|
||||
Popular
|
||||
</span>
|
||||
)}
|
||||
{isCurrentPlan && (
|
||||
<span className="bg-secondary/10 text-secondary text-[10px] font-medium px-1.5 py-0.5 rounded-full">
|
||||
Current
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="text-xs text-muted-foreground mt-0.5">{tier.description}</div>
|
||||
</div>
|
||||
|
||||
{!isCurrentPlan && accountId && (
|
||||
<form className="mt-2">
|
||||
<input type="hidden" name="accountId" value={accountId} />
|
||||
<input type="hidden" name="returnUrl" value={returnUrl} />
|
||||
<input type="hidden" name="planId" value={SUBSCRIPTION_PLANS[tier.name.toUpperCase() as keyof typeof SUBSCRIPTION_PLANS]} />
|
||||
{isManaged ? (
|
||||
<SubmitButton
|
||||
pendingText="Loading..."
|
||||
formAction={setupNewSubscription}
|
||||
className={cn(
|
||||
"w-full h-10 rounded-full font-medium transition-colors",
|
||||
tier.buttonColor
|
||||
<div className="text-right">
|
||||
<div className="flex items-baseline">
|
||||
<PriceDisplay tier={tier} isCompact={true} />
|
||||
<span className="text-xs text-muted-foreground ml-1">
|
||||
{tier.price !== "$0" ? "/mo" : ""}
|
||||
</span>
|
||||
</div>
|
||||
<div className="text-[10px] text-muted-foreground mt-0.5">
|
||||
{tier.hours}/month
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mb-2.5">
|
||||
<div className="text-[10px] text-muted-foreground leading-tight max-h-[40px] overflow-y-auto pr-1">
|
||||
{tier.features.map((feature, index) => (
|
||||
<span key={index} className="whitespace-normal">
|
||||
{index > 0 && ' • '}
|
||||
{feature}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
// Standard layout for normal view
|
||||
<>
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<h3 className="text-lg font-medium">{tier.name}</h3>
|
||||
<div className="flex gap-1">
|
||||
{tier.isPopular && (
|
||||
<span className="bg-primary/10 text-primary text-xs font-medium px-2 py-0.5 rounded-full">
|
||||
Popular
|
||||
</span>
|
||||
)}
|
||||
>
|
||||
{tier.buttonText}
|
||||
</SubmitButton>
|
||||
) : (
|
||||
<Button
|
||||
className={cn(
|
||||
"w-full h-10 rounded-full font-medium transition-colors",
|
||||
tier.buttonColor
|
||||
{isCurrentPlan && (
|
||||
<span className="bg-secondary/10 text-secondary text-xs font-medium px-2 py-0.5 rounded-full">
|
||||
Current
|
||||
</span>
|
||||
)}
|
||||
onClick={() => onPlanSelect?.(SUBSCRIPTION_PLANS[tier.name.toUpperCase() as keyof typeof SUBSCRIPTION_PLANS])}
|
||||
>
|
||||
{tier.buttonText}
|
||||
</Button>
|
||||
)}
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-baseline mb-1">
|
||||
<PriceDisplay tier={tier} />
|
||||
<span className="text-muted-foreground ml-2">
|
||||
{tier.price !== "$0" ? "/month" : ""}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-medium bg-secondary/10 text-secondary mb-4">
|
||||
{tier.hours}/month
|
||||
</div>
|
||||
|
||||
<p className="text-muted-foreground mb-6">{tier.description}</p>
|
||||
|
||||
<div className="mb-6">
|
||||
<div className="text-sm text-muted-foreground space-y-2">
|
||||
{tier.features.map((feature, index) => (
|
||||
<div key={index} className="flex items-start gap-2">
|
||||
<div className="size-5 rounded-full bg-primary/10 flex items-center justify-center mt-0.5">
|
||||
<svg
|
||||
width="12"
|
||||
height="12"
|
||||
viewBox="0 0 12 12"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className="text-primary"
|
||||
>
|
||||
<path
|
||||
d="M2.5 6L5 8.5L9.5 4"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<span>{feature}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
<div className="space-y-4">
|
||||
{tier.name !== "Free" && (
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Everything in {tier.name === "Basic" ? "Free" : "Basic"} +
|
||||
</p>
|
||||
)}
|
||||
<ul className="space-y-3">
|
||||
{tier.features.map((feature) => (
|
||||
<li key={feature} className="flex items-center gap-2 text-muted-foreground">
|
||||
<div className="size-5 rounded-full bg-primary/10 flex items-center justify-center">
|
||||
<svg
|
||||
width="12"
|
||||
height="12"
|
||||
viewBox="0 0 12 12"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className="text-primary"
|
||||
>
|
||||
<path
|
||||
d="M2.5 6L5 8.5L9.5 4"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<span className="text-sm">{feature}</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
|
||||
{(tier as any).showContactSales && (
|
||||
<Button
|
||||
variant="outline"
|
||||
className="w-full h-10 rounded-full font-medium transition-colors mt-4"
|
||||
onClick={() => window.open('mailto:support@kortix.ai?subject=Enterprise Plan Inquiry', '_blank')}
|
||||
<form>
|
||||
<input type="hidden" name="accountId" value={accountId} />
|
||||
<input type="hidden" name="returnUrl" value={returnUrl} />
|
||||
<input type="hidden" name="planId" value={SUBSCRIPTION_PLANS[tier.name.toUpperCase() as keyof typeof SUBSCRIPTION_PLANS]} />
|
||||
{isManaged ? (
|
||||
<SubmitButton
|
||||
pendingText="..."
|
||||
formAction={setupNewSubscription}
|
||||
// disabled={isCurrentPlan}
|
||||
className={cn(
|
||||
"w-full font-medium transition-colors",
|
||||
isCompact
|
||||
? "h-7 rounded-md text-xs"
|
||||
: "h-10 rounded-full text-sm",
|
||||
isCurrentPlan
|
||||
? "bg-muted text-muted-foreground hover:bg-muted"
|
||||
: tier.buttonColor
|
||||
)}
|
||||
>
|
||||
Need more? Contact Sales
|
||||
{isCurrentPlan ? "Current Plan" : (tier.name === "Free" ? tier.buttonText : "Upgrade")}
|
||||
</SubmitButton>
|
||||
) : (
|
||||
<Button
|
||||
className={cn(
|
||||
"w-full font-medium transition-colors",
|
||||
isCompact
|
||||
? "h-7 rounded-md text-xs"
|
||||
: "h-10 rounded-full text-sm",
|
||||
isCurrentPlan
|
||||
? "bg-muted text-muted-foreground hover:bg-muted"
|
||||
: tier.buttonColor
|
||||
)}
|
||||
disabled={isCurrentPlan}
|
||||
onClick={() => onPlanSelect?.(SUBSCRIPTION_PLANS[tier.name.toUpperCase() as keyof typeof SUBSCRIPTION_PLANS])}
|
||||
>
|
||||
{isCurrentPlan ? "Current Plan" : (tier.name === "Free" ? tier.buttonText : "Upgrade")}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
|
|
|
@ -6,6 +6,7 @@ interface BillingErrorState {
|
|||
limit?: number;
|
||||
subscription?: {
|
||||
price_id?: string;
|
||||
plan_name?: string;
|
||||
current_usage?: number;
|
||||
limit?: number;
|
||||
};
|
||||
|
@ -15,8 +16,20 @@ export function useBillingError() {
|
|||
const [billingError, setBillingError] = useState<BillingErrorState | null>(null);
|
||||
|
||||
const handleBillingError = useCallback((error: any) => {
|
||||
// Check if it's a billing error (402 status or message contains 'Payment Required')
|
||||
// Case 1: Error is already a formatted billing error detail object
|
||||
if (error && (error.message || error.subscription)) {
|
||||
setBillingError({
|
||||
message: error.message || "You've reached your monthly usage limit.",
|
||||
currentUsage: error.currentUsage || error.subscription?.current_usage,
|
||||
limit: error.limit || error.subscription?.limit,
|
||||
subscription: error.subscription || {}
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
// Case 2: Error is an HTTP error response
|
||||
if (error.status === 402 || (error.message && error.message.includes('Payment Required'))) {
|
||||
// Try to get details from error.data.detail (common API pattern)
|
||||
const errorDetail = error.data?.detail || {};
|
||||
const subscription = errorDetail.subscription || {};
|
||||
|
||||
|
@ -28,6 +41,8 @@ export function useBillingError() {
|
|||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
// Not a billing error
|
||||
return false;
|
||||
}, []);
|
||||
|
||||
|
|
Loading…
Reference in New Issue