Skip to content

Commit 6058163

Browse files
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>
1 parent 6126b20 commit 6058163

9 files changed

Lines changed: 93 additions & 28 deletions

File tree

frontend/src/billing/billingApi.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ export type BillingStatus = {
1818
total_tokens: number | null;
1919
used_tokens: number | null;
2020
usage_reset_date: string | null;
21+
api_credit_balance?: number;
2122
};
2223

2324
type BillingRecurringInfo = {

frontend/src/components/BillingStatus.tsx

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,9 @@ export function BillingStatus() {
5050
const isFree = billingStatus.product_name.toLowerCase().includes("free");
5151
const isMax = billingStatus.product_name.toLowerCase().includes("max");
5252

53+
const hasApiAccess =
54+
billingStatus.product_name?.toLowerCase().includes("pro") || isMax || isTeamPlan;
55+
5356
const getChatsText = () => {
5457
if (isFree) {
5558
if (billingStatus.chats_remaining === null || billingStatus.chats_remaining <= 0) {
@@ -59,9 +62,13 @@ export function BillingStatus() {
5962
}
6063
if (!billingStatus.can_chat) {
6164
if (isMax) {
62-
return "Contact us to increase your limits";
65+
return hasApiAccess
66+
? "Purchase API credits or contact us to increase limits"
67+
: "Contact us to increase your limits";
6368
}
64-
return "You've run out of messages, upgrade to keep chatting!";
69+
return hasApiAccess
70+
? "Upgrade your plan or purchase API credits to keep chatting!"
71+
: "You've run out of messages, upgrade to keep chatting!";
6572
}
6673

6774
// Show team name for team plans

frontend/src/components/CreditUsage.tsx

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,17 +22,25 @@ export function CreditUsage() {
2222
return null;
2323
}
2424

25+
// Check if user has API credits - always show if they have any
26+
const hasApiCredits =
27+
billingStatus.api_credit_balance !== undefined && billingStatus.api_credit_balance > 0;
28+
2529
// Set bar color based on usage
2630
const getBarColor = () => {
2731
if (percentUsed >= 90) return "rgb(239, 68, 68)"; // Tailwind red-500
2832
if (percentUsed >= 75) return "rgb(245, 158, 11)"; // Tailwind amber-500
2933
return "rgb(16, 185, 129)"; // Tailwind emerald-500
3034
};
3135

36+
const formatCredits = (credits: number) => {
37+
return new Intl.NumberFormat("en-US").format(credits);
38+
};
39+
3240
return (
3341
<div className="px-2 py-2 text-xs text-muted-foreground">
3442
<div className="mb-1 flex justify-between">
35-
<span>Credit Usage</span>
43+
<span>Plan Credits</span>
3644
<span>{roundedPercent}%</span>
3745
</div>
3846
<div className="h-2.5 w-full overflow-hidden rounded-full bg-muted">
@@ -44,8 +52,13 @@ export function CreditUsage() {
4452
}}
4553
/>
4654
</div>
47-
<div className="mt-1 text-xs text-right">
48-
{formatResetDate(billingStatus.usage_reset_date)}
55+
<div className="mt-1 flex justify-between text-xs">
56+
{hasApiCredits && (
57+
<span>+ {formatCredits(billingStatus.api_credit_balance ?? 0)} API credits</span>
58+
)}
59+
<span className={hasApiCredits ? "" : "ml-auto"}>
60+
{formatResetDate(billingStatus.usage_reset_date)}
61+
</span>
4962
</div>
5063
</div>
5164
);

frontend/src/components/UpgradePromptDialog.tsx

Lines changed: 29 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,8 @@ import {
1616
FileText,
1717
Gauge,
1818
MessageCircle,
19-
Globe
19+
Globe,
20+
Coins
2021
} from "lucide-react";
2122
import { useNavigate } from "@tanstack/react-router";
2223
import { useLocalState } from "@/state/useLocalState";
@@ -42,6 +43,11 @@ export function UpgradePromptDialog({
4243
navigate({ to: "/pricing" });
4344
};
4445

46+
const handleBuyCredits = () => {
47+
onOpenChange(false);
48+
navigate({ to: "/", search: { api_settings: true } });
49+
};
50+
4551
const handleNewChat = () => {
4652
onOpenChange(false);
4753
// Trigger new chat event
@@ -57,6 +63,7 @@ export function UpgradePromptDialog({
5763
const isFreeTier = !localState.billingStatus?.product_name || currentPlan === "free";
5864
const isPro = currentPlan.includes("pro") && !currentPlan.includes("max");
5965
const isMax = currentPlan.includes("max");
66+
const hasApiAccess = isPro || isMax || currentPlan.includes("team");
6067

6168
const getNextPlan = () => {
6269
if (isFreeTier) return "Pro";
@@ -130,8 +137,8 @@ export function UpgradePromptDialog({
130137
description: isFreeTier
131138
? "You've reached your daily free tier limit. Upgrade to Pro for unlimited daily usage."
132139
: isPro
133-
? "You've reached your Pro plan's monthly limit. Upgrade to Max for 10x more usage."
134-
: "You've reached your monthly usage limit. Please wait for the next billing cycle.",
140+
? "You've reached your Pro plan's monthly limit. Upgrade to Max for 10x more usage, or purchase API credits to continue chatting."
141+
: "You've reached your monthly usage limit. Purchase API credits to continue chatting, or wait for the next billing cycle.",
135142
requiredPlan: nextPlan,
136143
benefits: isFreeTier
137144
? [
@@ -147,11 +154,12 @@ export function UpgradePromptDialog({
147154
"10x more monthly messages with Max plan",
148155
"Access to all premium models including DeepSeek R1",
149156
"Highest priority during peak times",
150-
"Maximum rate limits for power users"
157+
"Maximum rate limits for power users",
158+
"Or purchase API credits to keep chatting now"
151159
]
152160
: [
153161
"You're already on our highest individual plan",
154-
"Consider Team plans for shared usage",
162+
"Purchase API credits to extend your usage",
155163
"Monthly usage automatically refreshes",
156164
"Contact support for custom enterprise plans"
157165
]
@@ -230,23 +238,30 @@ export function UpgradePromptDialog({
230238
) : null}
231239
</div>
232240

233-
<DialogFooter className="gap-2 sm:gap-0">
241+
<DialogFooter className="flex flex-col gap-2 sm:flex-col sm:justify-stretch sm:space-x-0">
242+
{(info.requiredPlan !== "Max" || !isMax) && (
243+
<Button onClick={handleUpgrade} className="w-full gap-2">
244+
<Sparkles className="h-4 w-4" />
245+
{isFreeTier ? "Upgrade to Pro" : isPro ? "Upgrade to Max" : "View Plans"}
246+
</Button>
247+
)}
248+
{/* Show Buy Credits button for paid users hitting usage limits */}
249+
{feature === "usage" && hasApiAccess && (
250+
<Button variant="outline" onClick={handleBuyCredits} className="w-full gap-2">
251+
<Coins className="h-4 w-4" />
252+
Buy API Credits
253+
</Button>
254+
)}
234255
{/* Show "Start New Chat" for free tier conversation limit, "Maybe Later" for others */}
235256
{feature === "tokens" && isFreeTier ? (
236-
<Button variant="outline" onClick={handleNewChat}>
257+
<Button variant="ghost" onClick={handleNewChat} className="w-full">
237258
Start New Chat
238259
</Button>
239260
) : (
240-
<Button variant="outline" onClick={() => onOpenChange(false)}>
261+
<Button variant="ghost" onClick={() => onOpenChange(false)} className="w-full">
241262
Maybe Later
242263
</Button>
243264
)}
244-
{(info.requiredPlan !== "Max" || !isMax) && (
245-
<Button onClick={handleUpgrade} className="gap-2">
246-
<Sparkles className="h-4 w-4" />
247-
{isFreeTier ? "Upgrade to Pro" : isPro ? "Upgrade to Max" : "View Plans"}
248-
</Button>
249-
)}
250265
</DialogFooter>
251266
</DialogContent>
252267
</Dialog>

frontend/src/components/apikeys/ApiCreditsSection.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -193,7 +193,7 @@ export function ApiCreditsSection({ showSuccessMessage = false }: ApiCreditsSect
193193
{formatCredits(creditBalance?.balance || 0)}
194194
</p>
195195
<p className="text-xs text-muted-foreground mt-1">
196-
$1 per 1,000 credits • Use for API requests
196+
$1 per 1,000 credits • Extends your subscription when plan credits run out
197197
</p>
198198
</div>
199199
</div>

frontend/src/components/apikeys/ApiKeyDashboard.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -162,9 +162,9 @@ export function ApiKeyDashboard({ showCreditSuccessMessage = false }: ApiKeyDash
162162
<Rocket className="h-5 w-5 text-green-500" />
163163
</div>
164164
<div>
165-
<h3 className="font-semibold">Pay-As-You-Go Credits</h3>
165+
<h3 className="font-semibold">Extend Your Subscription</h3>
166166
<p className="text-sm text-muted-foreground">
167-
Purchase credits for API usage at just $1 per 1,000 credits
167+
Purchase API credits to extend your usage when plan credits run out
168168
</p>
169169
</div>
170170
</div>
@@ -183,7 +183,7 @@ export function ApiKeyDashboard({ showCreditSuccessMessage = false }: ApiKeyDash
183183
</Button>
184184

185185
<p className="text-xs text-muted-foreground text-center">
186-
Unlock API access, increased limits, and premium features
186+
Use your plan credits via API, and purchase extra credits to extend your usage
187187
</p>
188188
</div>
189189
</div>

frontend/src/config/pricingConfig.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -139,7 +139,7 @@ export const PRICING_PLANS: PricingPlan[] = [
139139
icon: <Check className="w-4 h-4 text-green-500" />
140140
},
141141
{
142-
text: "API Access",
142+
text: "API Access (use plan credits via API)",
143143
included: true,
144144
icon: <Check className="w-4 h-4 text-green-500" />
145145
}
@@ -189,7 +189,7 @@ export const PRICING_PLANS: PricingPlan[] = [
189189
icon: <Check className="w-4 h-4 text-green-500" />
190190
},
191191
{
192-
text: "API Access",
192+
text: "API Access (use plan credits via API)",
193193
included: true,
194194
icon: <Check className="w-4 h-4 text-green-500" />
195195
}
@@ -248,7 +248,7 @@ export const PRICING_PLANS: PricingPlan[] = [
248248
icon: <Check className="w-4 h-4 text-green-500" />
249249
},
250250
{
251-
text: "API Access",
251+
text: "API Access (use plan credits via API)",
252252
included: true,
253253
icon: <Check className="w-4 h-4 text-green-500" />
254254
}

frontend/src/routes/index.tsx

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ type IndexSearchOptions = {
2020
next?: string;
2121
team_setup?: boolean;
2222
credits_success?: boolean;
23+
api_settings?: boolean;
2324
};
2425

2526
function validateSearch(search: Record<string, unknown>): IndexSearchOptions {
@@ -28,7 +29,9 @@ function validateSearch(search: Record<string, unknown>): IndexSearchOptions {
2829
next: search.next ? (search.next as string) : undefined,
2930
team_setup: search?.team_setup === true || search?.team_setup === "true" ? true : undefined,
3031
credits_success:
31-
search?.credits_success === true || search?.credits_success === "true" ? true : undefined
32+
search?.credits_success === true || search?.credits_success === "true" ? true : undefined,
33+
api_settings:
34+
search?.api_settings === true || search?.api_settings === "true" ? true : undefined
3235
};
3336
}
3437

@@ -43,7 +46,7 @@ function Index() {
4346
const queryClient = useQueryClient();
4447
const { setBillingStatus, billingStatus } = useLocalState();
4548

46-
const { login, next, team_setup, credits_success } = Route.useSearch();
49+
const { login, next, team_setup, credits_success, api_settings } = Route.useSearch();
4750

4851
// Modal states
4952
const [teamDialogOpen, setTeamDialogOpen] = useState(false);
@@ -119,6 +122,15 @@ function Index() {
119122
}
120123
}, [credits_success, os.auth.user, navigate, queryClient]);
121124

125+
// Handle api_settings - open API key dialog directly
126+
useEffect(() => {
127+
if (api_settings && os.auth.user) {
128+
setApiKeyDialogOpen(true);
129+
// Clear the query param to prevent re-opening on refresh
130+
navigate({ to: "/", replace: true });
131+
}
132+
}, [api_settings, os.auth.user, navigate]);
133+
122134
// Check if guest user needs to pay
123135
const isGuestUser = os.auth.user?.user.login_method?.toLowerCase() === "guest";
124136
const isOnFreePlan = billingStatus?.product_name?.toLowerCase().includes("free") ?? false;

frontend/src/routes/pricing.tsx

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -156,6 +156,23 @@ function PricingFAQ() {
156156
</div>
157157
</details>
158158

159+
<details className="group">
160+
<summary className="cursor-pointer text-lg font-medium hover:text-foreground/80">
161+
Can I use my subscription for API access?
162+
</summary>
163+
<div className="mt-4 text-[hsl(var(--marketing-text-muted))] space-y-2">
164+
<p>
165+
Yes! Pro, Max, and Team plans include API access. Your subscription credits work
166+
seamlessly with the API.
167+
</p>
168+
<ul className="list-disc list-inside space-y-1 ml-4">
169+
<li>Use your plan credits via the API</li>
170+
<li>When plan credits run out, API credits kick in automatically</li>
171+
<li>Purchase additional API credits to extend your usage anytime</li>
172+
</ul>
173+
</div>
174+
</details>
175+
159176
<details className="group">
160177
<summary className="cursor-pointer text-lg font-medium hover:text-foreground/80">
161178
How did you build this?

0 commit comments

Comments
 (0)