From 6058163c33f08105e8d842bdf595bc567e104ed6 Mon Sep 17 00:00:00 2001 From: Tony Giorgio Date: Fri, 30 Jan 2026 10:50:10 -0600 Subject: [PATCH] feat: merge subscription credits with API credits UI updates - Add api_credit_balance field to BillingStatus type - Show API credit balance in CreditUsage when plan is >=75% used - Add 'Buy API Credits' button in UpgradePromptDialog for paid users - Update BillingStatus messaging to mention API credits option - Add FAQ about using subscription for API access - Update API Access feature descriptions in pricing config - Update ApiCreditsSection description text - Add api_settings URL param to open API settings dialog Closes #406 Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com> --- frontend/src/billing/billingApi.ts | 1 + frontend/src/components/BillingStatus.tsx | 11 ++++- frontend/src/components/CreditUsage.tsx | 19 ++++++-- .../src/components/UpgradePromptDialog.tsx | 43 +++++++++++++------ .../components/apikeys/ApiCreditsSection.tsx | 2 +- .../components/apikeys/ApiKeyDashboard.tsx | 6 +-- frontend/src/config/pricingConfig.tsx | 6 +-- frontend/src/routes/index.tsx | 16 ++++++- frontend/src/routes/pricing.tsx | 17 ++++++++ 9 files changed, 93 insertions(+), 28 deletions(-) diff --git a/frontend/src/billing/billingApi.ts b/frontend/src/billing/billingApi.ts index 74241622..26bb287c 100644 --- a/frontend/src/billing/billingApi.ts +++ b/frontend/src/billing/billingApi.ts @@ -18,6 +18,7 @@ export type BillingStatus = { total_tokens: number | null; used_tokens: number | null; usage_reset_date: string | null; + api_credit_balance?: number; }; type BillingRecurringInfo = { diff --git a/frontend/src/components/BillingStatus.tsx b/frontend/src/components/BillingStatus.tsx index 9ff291df..6f208fdf 100644 --- a/frontend/src/components/BillingStatus.tsx +++ b/frontend/src/components/BillingStatus.tsx @@ -50,6 +50,9 @@ export function BillingStatus() { const isFree = billingStatus.product_name.toLowerCase().includes("free"); const isMax = billingStatus.product_name.toLowerCase().includes("max"); + const hasApiAccess = + billingStatus.product_name?.toLowerCase().includes("pro") || isMax || isTeamPlan; + const getChatsText = () => { if (isFree) { if (billingStatus.chats_remaining === null || billingStatus.chats_remaining <= 0) { @@ -59,9 +62,13 @@ export function BillingStatus() { } if (!billingStatus.can_chat) { if (isMax) { - return "Contact us to increase your limits"; + return hasApiAccess + ? "Purchase API credits or contact us to increase limits" + : "Contact us to increase your limits"; } - return "You've run out of messages, upgrade to keep chatting!"; + return hasApiAccess + ? "Upgrade your plan or purchase API credits to keep chatting!" + : "You've run out of messages, upgrade to keep chatting!"; } // Show team name for team plans diff --git a/frontend/src/components/CreditUsage.tsx b/frontend/src/components/CreditUsage.tsx index 17eb8882..63c9c5fc 100644 --- a/frontend/src/components/CreditUsage.tsx +++ b/frontend/src/components/CreditUsage.tsx @@ -22,6 +22,10 @@ export function CreditUsage() { return null; } + // Check if user has API credits - always show if they have any + const hasApiCredits = + billingStatus.api_credit_balance !== undefined && billingStatus.api_credit_balance > 0; + // Set bar color based on usage const getBarColor = () => { if (percentUsed >= 90) return "rgb(239, 68, 68)"; // Tailwind red-500 @@ -29,10 +33,14 @@ export function CreditUsage() { return "rgb(16, 185, 129)"; // Tailwind emerald-500 }; + const formatCredits = (credits: number) => { + return new Intl.NumberFormat("en-US").format(credits); + }; + return (
- Credit Usage + Plan Credits {roundedPercent}%
@@ -44,8 +52,13 @@ export function CreditUsage() { }} />
-
- {formatResetDate(billingStatus.usage_reset_date)} +
+ {hasApiCredits && ( + + {formatCredits(billingStatus.api_credit_balance ?? 0)} API credits + )} + + {formatResetDate(billingStatus.usage_reset_date)} +
); diff --git a/frontend/src/components/UpgradePromptDialog.tsx b/frontend/src/components/UpgradePromptDialog.tsx index 71941017..5e9ea804 100644 --- a/frontend/src/components/UpgradePromptDialog.tsx +++ b/frontend/src/components/UpgradePromptDialog.tsx @@ -16,7 +16,8 @@ import { FileText, Gauge, MessageCircle, - Globe + Globe, + Coins } from "lucide-react"; import { useNavigate } from "@tanstack/react-router"; import { useLocalState } from "@/state/useLocalState"; @@ -42,6 +43,11 @@ export function UpgradePromptDialog({ navigate({ to: "/pricing" }); }; + const handleBuyCredits = () => { + onOpenChange(false); + navigate({ to: "/", search: { api_settings: true } }); + }; + const handleNewChat = () => { onOpenChange(false); // Trigger new chat event @@ -57,6 +63,7 @@ export function UpgradePromptDialog({ const isFreeTier = !localState.billingStatus?.product_name || currentPlan === "free"; const isPro = currentPlan.includes("pro") && !currentPlan.includes("max"); const isMax = currentPlan.includes("max"); + const hasApiAccess = isPro || isMax || currentPlan.includes("team"); const getNextPlan = () => { if (isFreeTier) return "Pro"; @@ -130,8 +137,8 @@ export function UpgradePromptDialog({ description: isFreeTier ? "You've reached your daily free tier limit. Upgrade to Pro for unlimited daily usage." : isPro - ? "You've reached your Pro plan's monthly limit. Upgrade to Max for 10x more usage." - : "You've reached your monthly usage limit. Please wait for the next billing cycle.", + ? "You've reached your Pro plan's monthly limit. Upgrade to Max for 10x more usage, or purchase API credits to continue chatting." + : "You've reached your monthly usage limit. Purchase API credits to continue chatting, or wait for the next billing cycle.", requiredPlan: nextPlan, benefits: isFreeTier ? [ @@ -147,11 +154,12 @@ export function UpgradePromptDialog({ "10x more monthly messages with Max plan", "Access to all premium models including DeepSeek R1", "Highest priority during peak times", - "Maximum rate limits for power users" + "Maximum rate limits for power users", + "Or purchase API credits to keep chatting now" ] : [ "You're already on our highest individual plan", - "Consider Team plans for shared usage", + "Purchase API credits to extend your usage", "Monthly usage automatically refreshes", "Contact support for custom enterprise plans" ] @@ -230,23 +238,30 @@ export function UpgradePromptDialog({ ) : null}
- + + {(info.requiredPlan !== "Max" || !isMax) && ( + + )} + {/* Show Buy Credits button for paid users hitting usage limits */} + {feature === "usage" && hasApiAccess && ( + + )} {/* Show "Start New Chat" for free tier conversation limit, "Maybe Later" for others */} {feature === "tokens" && isFreeTier ? ( - ) : ( - )} - {(info.requiredPlan !== "Max" || !isMax) && ( - - )} diff --git a/frontend/src/components/apikeys/ApiCreditsSection.tsx b/frontend/src/components/apikeys/ApiCreditsSection.tsx index a5438931..7b2c22bd 100644 --- a/frontend/src/components/apikeys/ApiCreditsSection.tsx +++ b/frontend/src/components/apikeys/ApiCreditsSection.tsx @@ -193,7 +193,7 @@ export function ApiCreditsSection({ showSuccessMessage = false }: ApiCreditsSect {formatCredits(creditBalance?.balance || 0)}

- $1 per 1,000 credits • Use for API requests + $1 per 1,000 credits • Extends your subscription when plan credits run out

diff --git a/frontend/src/components/apikeys/ApiKeyDashboard.tsx b/frontend/src/components/apikeys/ApiKeyDashboard.tsx index be7d18d5..66a8e2e3 100644 --- a/frontend/src/components/apikeys/ApiKeyDashboard.tsx +++ b/frontend/src/components/apikeys/ApiKeyDashboard.tsx @@ -162,9 +162,9 @@ export function ApiKeyDashboard({ showCreditSuccessMessage = false }: ApiKeyDash
-

Pay-As-You-Go Credits

+

Extend Your Subscription

- Purchase credits for API usage at just $1 per 1,000 credits + Purchase API credits to extend your usage when plan credits run out

@@ -183,7 +183,7 @@ export function ApiKeyDashboard({ showCreditSuccessMessage = false }: ApiKeyDash

- Unlock API access, increased limits, and premium features + Use your plan credits via API, and purchase extra credits to extend your usage

diff --git a/frontend/src/config/pricingConfig.tsx b/frontend/src/config/pricingConfig.tsx index 942e9dc5..783a0696 100644 --- a/frontend/src/config/pricingConfig.tsx +++ b/frontend/src/config/pricingConfig.tsx @@ -139,7 +139,7 @@ export const PRICING_PLANS: PricingPlan[] = [ icon: }, { - text: "API Access", + text: "API Access (use plan credits via API)", included: true, icon: } @@ -189,7 +189,7 @@ export const PRICING_PLANS: PricingPlan[] = [ icon: }, { - text: "API Access", + text: "API Access (use plan credits via API)", included: true, icon: } @@ -248,7 +248,7 @@ export const PRICING_PLANS: PricingPlan[] = [ icon: }, { - text: "API Access", + text: "API Access (use plan credits via API)", included: true, icon: } diff --git a/frontend/src/routes/index.tsx b/frontend/src/routes/index.tsx index 013dbe39..4e388742 100644 --- a/frontend/src/routes/index.tsx +++ b/frontend/src/routes/index.tsx @@ -20,6 +20,7 @@ type IndexSearchOptions = { next?: string; team_setup?: boolean; credits_success?: boolean; + api_settings?: boolean; }; function validateSearch(search: Record): IndexSearchOptions { @@ -28,7 +29,9 @@ function validateSearch(search: Record): IndexSearchOptions { next: search.next ? (search.next as string) : undefined, team_setup: search?.team_setup === true || search?.team_setup === "true" ? true : undefined, credits_success: - search?.credits_success === true || search?.credits_success === "true" ? true : undefined + search?.credits_success === true || search?.credits_success === "true" ? true : undefined, + api_settings: + search?.api_settings === true || search?.api_settings === "true" ? true : undefined }; } @@ -43,7 +46,7 @@ function Index() { const queryClient = useQueryClient(); const { setBillingStatus, billingStatus } = useLocalState(); - const { login, next, team_setup, credits_success } = Route.useSearch(); + const { login, next, team_setup, credits_success, api_settings } = Route.useSearch(); // Modal states const [teamDialogOpen, setTeamDialogOpen] = useState(false); @@ -119,6 +122,15 @@ function Index() { } }, [credits_success, os.auth.user, navigate, queryClient]); + // Handle api_settings - open API key dialog directly + useEffect(() => { + if (api_settings && os.auth.user) { + setApiKeyDialogOpen(true); + // Clear the query param to prevent re-opening on refresh + navigate({ to: "/", replace: true }); + } + }, [api_settings, os.auth.user, navigate]); + // Check if guest user needs to pay const isGuestUser = os.auth.user?.user.login_method?.toLowerCase() === "guest"; const isOnFreePlan = billingStatus?.product_name?.toLowerCase().includes("free") ?? false; diff --git a/frontend/src/routes/pricing.tsx b/frontend/src/routes/pricing.tsx index cfd562f0..5efd4ea1 100644 --- a/frontend/src/routes/pricing.tsx +++ b/frontend/src/routes/pricing.tsx @@ -156,6 +156,23 @@ function PricingFAQ() { +
+ + Can I use my subscription for API access? + +
+

+ Yes! Pro, Max, and Team plans include API access. Your subscription credits work + seamlessly with the API. +

+
    +
  • Use your plan credits via the API
  • +
  • When plan credits run out, API credits kick in automatically
  • +
  • Purchase additional API credits to extend your usage anytime
  • +
+
+
+
How did you build this?