From 8751a1a7162adc5a89ef0634fa4394a7d517ef22 Mon Sep 17 00:00:00 2001 From: Adam Cohen Hillel Date: Mon, 21 Apr 2025 16:36:40 +0100 Subject: [PATCH] billing popup --- .../src/app/(dashboard)/dashboard/page.tsx | 124 ++++++++- .../components/billing/BillingErrorAlert.tsx | 40 +-- .../src/components/billing/PlanComparison.tsx | 259 +++++++++++------- frontend/src/hooks/useBillingError.ts | 17 +- 4 files changed, 308 insertions(+), 132 deletions(-) diff --git a/frontend/src/app/(dashboard)/dashboard/page.tsx b/frontend/src/app/(dashboard)/dashboard/page.tsx index 09df65fd..8e19f1de 100644 --- a/frontend/src/app/(dashboard)/dashboard/page.tsx +++ b/frontend/src/app/(dashboard)/dashboard/page.tsx @@ -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 - }); - - // 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}`); + 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); + + // 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} /> + + {/* Billing Error Alert */} + ); } diff --git a/frontend/src/components/billing/BillingErrorAlert.tsx b/frontend/src/components/billing/BillingErrorAlert.tsx index 6772fcdb..f9ccc299 100644 --- a/frontend/src/components/billing/BillingErrorAlert.tsx +++ b/frontend/src/components/billing/BillingErrorAlert.tsx @@ -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 */} -
+
{/* Close button */} {onDismiss && ( )} {/* Header */} -
-
- +
+
+
-

+

Usage Limit Reached

-

+

{message || "You've reached your monthly usage limit."}

{/* Usage Stats */} {currentUsage !== undefined && limit !== undefined && ( -
-
+
+
-

Current Usage

-

{currentUsage} hours

+

Usage

+

{currentUsage}h

-

Monthly Limit

-

{limit} hours

+

Limit

+

{limit}h

-
+
{/* Dismiss Button */} {onDismiss && ( - )} - - )} - -
- {tier.name !== "Free" && ( -

- Everything in {tier.name === "Basic" ? "Free" : "Basic"} + -

- )} -
    - {tier.features.map((feature) => ( -
  • -
    - - - + {isCompact ? ( + // Compact layout for modal + <> +
    +
    +
    +

    {tier.name}

    + {tier.isPopular && ( + + Popular + + )} + {isCurrentPlan && ( + + Current + + )}
    - {feature} -
  • - ))} -
- - {(tier as any).showContactSales && ( -
+ +
+
+ + + {tier.price !== "$0" ? "/mo" : ""} + +
+
+ {tier.hours}/month +
+
+
+ +
+
+ {tier.features.map((feature, index) => ( + + {index > 0 && ' • '} + {feature} + + ))} +
+
+ + ) : ( + // Standard layout for normal view + <> +
+

{tier.name}

+
+ {tier.isPopular && ( + + Popular + + )} + {isCurrentPlan && ( + + Current + + )} +
+
+ +
+ + + {tier.price !== "$0" ? "/month" : ""} + +
+ +
+ {tier.hours}/month +
+ +

{tier.description}

+ +
+
+ {tier.features.map((feature, index) => ( +
+
+ + + +
+ {feature} +
+ ))} +
+
+ + )} + +
+ + + + {isManaged ? ( + - Need more? Contact Sales + {isCurrentPlan ? "Current Plan" : (tier.name === "Free" ? tier.buttonText : "Upgrade")} + + ) : ( + )} -
+
); })} diff --git a/frontend/src/hooks/useBillingError.ts b/frontend/src/hooks/useBillingError.ts index 6577c9d4..58b125ee 100644 --- a/frontend/src/hooks/useBillingError.ts +++ b/frontend/src/hooks/useBillingError.ts @@ -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(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; }, []);