Skip to content

Commit 0a72e18

Browse files
committed
Refactor to pull out Subscription types
1 parent 7120b0e commit 0a72e18

File tree

7 files changed

+236
-279
lines changed

7 files changed

+236
-279
lines changed

cli/src/components/message-footer.tsx

Lines changed: 12 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -213,61 +213,43 @@ export const MessageFooter: React.FC<MessageFooterProps> = ({
213213
)
214214
}
215215

216-
/**
217-
* Shows either subscription indicator or credits count based on subscription status.
218-
* If user has an active subscription with remaining block credits, shows "✓ Strong".
219-
* If block is < 15% remaining, also shows the percentage.
220-
* Otherwise, shows the regular credits count.
221-
*/
222216
const CreditsOrSubscriptionIndicator: React.FC<{ credits: number }> = ({ credits }) => {
223217
const theme = useTheme()
224218
const { data: subscriptionData } = useSubscriptionQuery({
225-
refetchInterval: false, // Don't poll, just use cached data
219+
refetchInterval: false,
226220
refetchOnActivity: false,
227221
pauseWhenIdle: false,
228222
})
229223

230-
const hasActiveSubscription = subscriptionData?.hasSubscription === true
231-
const rateLimit = subscriptionData?.rateLimit
232-
const isLimited = rateLimit?.limited === true
224+
const activeSubscription = subscriptionData?.hasSubscription ? subscriptionData : null
225+
const rateLimit = activeSubscription?.rateLimit
233226

234-
// Calculate block remaining percentage
235227
const blockPercentRemaining = useMemo(() => {
236228
if (!rateLimit?.blockLimit || rateLimit.blockUsed == null) return null
237-
const remaining = rateLimit.blockLimit - rateLimit.blockUsed
238-
return Math.round((remaining / rateLimit.blockLimit) * 100)
229+
return Math.round(((rateLimit.blockLimit - rateLimit.blockUsed) / rateLimit.blockLimit) * 100)
239230
}, [rateLimit])
240231

241-
// Show subscription indicator if user has active subscription and block is not depleted
242-
const showSubscriptionIndicator = hasActiveSubscription && !isLimited && blockPercentRemaining !== null && blockPercentRemaining > 0
232+
const showSubscriptionIndicator =
233+
activeSubscription && !rateLimit?.limited && blockPercentRemaining != null && blockPercentRemaining > 0
243234

244235
if (showSubscriptionIndicator) {
245-
const showPercentage = blockPercentRemaining < 20
236+
const label = blockPercentRemaining < 20
237+
? `✓ ${SUBSCRIPTION_DISPLAY_NAME} (${blockPercentRemaining}% left)`
238+
: `✓ ${SUBSCRIPTION_DISPLAY_NAME}`
246239
return (
247240
<text
248241
attributes={TextAttributes.DIM}
249-
style={{
250-
wrapMode: 'none',
251-
fg: theme.success,
252-
marginTop: 0,
253-
marginBottom: 0,
254-
}}
242+
style={{ wrapMode: 'none', fg: theme.success, marginTop: 0, marginBottom: 0 }}
255243
>
256-
{showPercentage ? `✓ ${SUBSCRIPTION_DISPLAY_NAME} (${blockPercentRemaining}% left)` : `✓ ${SUBSCRIPTION_DISPLAY_NAME}`}
244+
{label}
257245
</text>
258246
)
259247
}
260248

261-
// Default: show credits count
262249
return (
263250
<text
264251
attributes={TextAttributes.DIM}
265-
style={{
266-
wrapMode: 'none',
267-
fg: theme.secondary,
268-
marginTop: 0,
269-
marginBottom: 0,
270-
}}
252+
style={{ wrapMode: 'none', fg: theme.secondary, marginTop: 0, marginBottom: 0 }}
271253
>
272254
{pluralize(credits, 'credit')}
273255
</text>

cli/src/components/subscription-limit-banner.tsx

Lines changed: 9 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ export const SubscriptionLimitBanner = () => {
2828
refetchInterval: 30 * 1000,
2929
})
3030

31-
const rateLimit = subscriptionData?.rateLimit
31+
const rateLimit = subscriptionData?.hasSubscription ? subscriptionData.rateLimit : undefined
3232
const remainingBalance = usageData?.remainingBalance ?? 0
3333
const hasAlaCarteCredits = remainingBalance > 0
3434

@@ -42,29 +42,16 @@ export const SubscriptionLimitBanner = () => {
4242
setAlwaysUseALaCarte(newValue)
4343
}
4444

45-
if (!subscriptionData) {
46-
return (
47-
<box style={{ width: '100%', paddingLeft: 1 }}>
48-
<text style={{ fg: theme.muted }}>Loading subscription data...</text>
49-
</box>
50-
)
51-
}
52-
53-
if (!rateLimit?.limited) {
45+
if (!subscriptionData || !rateLimit?.limited) {
5446
return null
5547
}
5648

57-
const isWeeklyLimit = rateLimit.reason === 'weekly_limit'
58-
const isBlockExhausted = rateLimit.reason === 'block_exhausted'
59-
60-
const weeklyRemaining = 100 - rateLimit.weeklyPercentUsed
61-
const weeklyResetsAt = rateLimit.weeklyResetsAt
62-
? new Date(rateLimit.weeklyResetsAt)
63-
: null
64-
65-
const blockResetsAt = rateLimit.blockResetsAt
66-
? new Date(rateLimit.blockResetsAt)
67-
: null
49+
const { reason, weeklyPercentUsed, weeklyResetsAt: weeklyResetsAtStr, blockResetsAt: blockResetsAtStr } = rateLimit
50+
const isWeeklyLimit = reason === 'weekly_limit'
51+
const isBlockExhausted = reason === 'block_exhausted'
52+
const weeklyRemaining = 100 - weeklyPercentUsed
53+
const weeklyResetsAt = weeklyResetsAtStr ? new Date(weeklyResetsAtStr) : null
54+
const blockResetsAt = blockResetsAtStr ? new Date(blockResetsAtStr) : null
6855

6956
const handleContinueWithCredits = () => {
7057
setInputMode('default')
@@ -137,7 +124,7 @@ export const SubscriptionLimitBanner = () => {
137124
<box style={{ flexDirection: 'row', alignItems: 'center', gap: 1, marginTop: 0 }}>
138125
<text style={{ fg: theme.muted }}>Weekly:</text>
139126
<ProgressBar value={weeklyRemaining} width={12} showPercentage={false} />
140-
<text style={{ fg: theme.muted }}>{rateLimit.weeklyPercentUsed}% used</text>
127+
<text style={{ fg: theme.muted }}>{weeklyPercentUsed}% used</text>
141128
</box>
142129

143130
{hasAlaCarteCredits && (

cli/src/components/usage-banner.tsx

Lines changed: 77 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { isClaudeOAuthValid } from '@codebuff/sdk'
22
import open from 'open'
3-
import React, { useEffect } from 'react'
3+
import React, { useEffect, useMemo } from 'react'
44

55
import { BottomBanner } from './bottom-banner'
66
import { Button } from './button'
@@ -105,66 +105,22 @@ export const UsageBanner = ({ showTime }: { showTime: number }) => {
105105
const adCredits = activeData.balanceBreakdown?.ad
106106
const renewalDate = activeData.next_quota_reset ? formatRenewalDate(activeData.next_quota_reset) : null
107107

108-
const hasSubscription = subscriptionData?.hasSubscription === true
109-
const rateLimit = subscriptionData?.rateLimit
110-
const subscriptionInfo = subscriptionData?.subscription
108+
const activeSubscription = subscriptionData?.hasSubscription ? subscriptionData : null
109+
const { rateLimit, subscription: subscriptionInfo, displayName } = activeSubscription ?? {}
111110

112111
return (
113112
<BottomBanner
114113
borderColorKey={isLoadingData ? 'muted' : colorLevel}
115114
onClose={() => setInputMode('default')}
116115
>
117116
<box style={{ flexDirection: 'column', gap: 0 }}>
118-
{/* Strong subscription section - only show if subscribed */}
119-
{hasSubscription && (
120-
<box style={{ flexDirection: 'column', marginBottom: 1 }}>
121-
<box style={{ flexDirection: 'row', gap: 1 }}>
122-
<text style={{ fg: theme.foreground }}>
123-
💪 {subscriptionData.displayName ?? 'Strong'} subscription
124-
</text>
125-
{subscriptionInfo?.tier && (
126-
<text style={{ fg: theme.muted }}>${subscriptionInfo.tier}/mo</text>
127-
)}
128-
</box>
129-
{isSubscriptionLoading ? (
130-
<text style={{ fg: theme.muted }}>Loading subscription data...</text>
131-
) : rateLimit ? (
132-
<box style={{ flexDirection: 'column', gap: 0 }}>
133-
{/* Block progress - always show for Strong subscription */}
134-
{(() => {
135-
const blockPercent = rateLimit.blockLimit != null && rateLimit.blockUsed != null
136-
? Math.max(0, 100 - Math.round((rateLimit.blockUsed / rateLimit.blockLimit) * 100))
137-
: 100
138-
return (
139-
<box style={{ flexDirection: 'row', alignItems: 'center', gap: 0 }}>
140-
<text style={{ fg: theme.muted }}>5-hour limit </text>
141-
<text style={{ fg: theme.muted }}>{`${blockPercent}%`.padStart(4)} </text>
142-
<ProgressBar value={blockPercent} width={12} showPercentage={false} />
143-
<text style={{ fg: theme.muted }}>
144-
{rateLimit.blockResetsAt
145-
? ` resets in ${formatResetTime(new Date(rateLimit.blockResetsAt))}`
146-
: ''}
147-
</text>
148-
</box>
149-
)
150-
})()}
151-
{/* Weekly progress */}
152-
{(() => {
153-
const weeklyPercent = 100 - rateLimit.weeklyPercentUsed
154-
return (
155-
<box style={{ flexDirection: 'row', alignItems: 'center', gap: 0 }}>
156-
<text style={{ fg: theme.muted }}>Weekly limit </text>
157-
<text style={{ fg: theme.muted }}>{`${weeklyPercent}%`.padStart(4)} </text>
158-
<ProgressBar value={weeklyPercent} width={12} showPercentage={false} />
159-
<text style={{ fg: theme.muted }}>
160-
{' '}resets in {formatResetTimeLong(rateLimit.weeklyResetsAt)}
161-
</text>
162-
</box>
163-
)
164-
})()}
165-
</box>
166-
) : null}
167-
</box>
117+
{activeSubscription && (
118+
<SubscriptionUsageSection
119+
displayName={displayName}
120+
subscriptionInfo={subscriptionInfo}
121+
rateLimit={rateLimit}
122+
isLoading={isSubscriptionLoading}
123+
/>
168124
)}
169125

170126
{/* Codebuff credits section - structured layout */}
@@ -190,7 +146,7 @@ export const UsageBanner = ({ showTime }: { showTime: number }) => {
190146
{adCredits != null && adCredits > 0 && (
191147
<text style={{ fg: theme.muted }}>{`(${adCredits} from ads)`}</text>
192148
)}
193-
{!hasSubscription && renewalDate && (
149+
{!activeSubscription && renewalDate && (
194150
<>
195151
<text style={{ fg: theme.muted }}>· Renews:</text>
196152
<text style={{ fg: theme.foreground }}>{renewalDate}</text>
@@ -239,3 +195,69 @@ export const UsageBanner = ({ showTime }: { showTime: number }) => {
239195
</BottomBanner>
240196
)
241197
}
198+
199+
interface SubscriptionUsageSectionProps {
200+
displayName?: string
201+
subscriptionInfo?: { tier: number }
202+
rateLimit?: {
203+
blockLimit?: number
204+
blockUsed?: number
205+
blockResetsAt?: string
206+
weeklyPercentUsed: number
207+
weeklyResetsAt: string
208+
}
209+
isLoading: boolean
210+
}
211+
212+
const SubscriptionUsageSection: React.FC<SubscriptionUsageSectionProps> = ({
213+
displayName,
214+
subscriptionInfo,
215+
rateLimit,
216+
isLoading,
217+
}) => {
218+
const theme = useTheme()
219+
220+
const blockPercent = useMemo(() => {
221+
if (rateLimit?.blockLimit == null || rateLimit.blockUsed == null) return 100
222+
return Math.max(0, 100 - Math.round((rateLimit.blockUsed / rateLimit.blockLimit) * 100))
223+
}, [rateLimit?.blockLimit, rateLimit?.blockUsed])
224+
225+
const weeklyPercent = rateLimit ? 100 - rateLimit.weeklyPercentUsed : 100
226+
227+
return (
228+
<box style={{ flexDirection: 'column', marginBottom: 1 }}>
229+
<box style={{ flexDirection: 'row', gap: 1 }}>
230+
<text style={{ fg: theme.foreground }}>
231+
💪 {displayName ?? 'Strong'} subscription
232+
</text>
233+
{subscriptionInfo?.tier && (
234+
<text style={{ fg: theme.muted }}>${subscriptionInfo.tier}/mo</text>
235+
)}
236+
</box>
237+
{isLoading ? (
238+
<text style={{ fg: theme.muted }}>Loading subscription data...</text>
239+
) : rateLimit ? (
240+
<box style={{ flexDirection: 'column', gap: 0 }}>
241+
<box style={{ flexDirection: 'row', alignItems: 'center', gap: 0 }}>
242+
<text style={{ fg: theme.muted }}>5-hour limit </text>
243+
<text style={{ fg: theme.muted }}>{`${blockPercent}%`.padStart(4)} </text>
244+
<ProgressBar value={blockPercent} width={12} showPercentage={false} />
245+
<text style={{ fg: theme.muted }}>
246+
{rateLimit.blockResetsAt
247+
? ` resets in ${formatResetTime(new Date(rateLimit.blockResetsAt))}`
248+
: ''}
249+
</text>
250+
</box>
251+
<box style={{ flexDirection: 'row', alignItems: 'center', gap: 0 }}>
252+
<text style={{ fg: theme.muted }}>Weekly limit </text>
253+
<text style={{ fg: theme.muted }}>{`${weeklyPercent}%`.padStart(4)} </text>
254+
<ProgressBar value={weeklyPercent} width={12} showPercentage={false} />
255+
<text style={{ fg: theme.muted }}>
256+
{' '}resets in {formatResetTimeLong(rateLimit.weeklyResetsAt)}
257+
</text>
258+
</box>
259+
</box>
260+
) : null}
261+
</box>
262+
)
263+
}

cli/src/hooks/use-subscription-query.ts

Lines changed: 5 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -4,52 +4,20 @@ import { getApiClient } from '../utils/codebuff-api'
44
import { logger as defaultLogger } from '../utils/logger'
55

66
import type { Logger } from '@codebuff/common/types/contracts/logger'
7+
import type { SubscriptionResponse } from '@codebuff/common/types/subscription'
8+
9+
export type { SubscriptionResponse }
710

811
export const subscriptionQueryKeys = {
912
all: ['subscription'] as const,
1013
current: () => [...subscriptionQueryKeys.all, 'current'] as const,
1114
}
1215

13-
export interface SubscriptionRateLimit {
14-
limited: boolean
15-
reason?: 'block_exhausted' | 'weekly_limit'
16-
canStartNewBlock: boolean
17-
blockUsed?: number
18-
blockLimit?: number
19-
blockResetsAt?: string
20-
weeklyUsed: number
21-
weeklyLimit: number
22-
weeklyResetsAt: string
23-
weeklyPercentUsed: number
24-
}
25-
26-
export interface SubscriptionInfo {
27-
status: string
28-
billingPeriodEnd: string
29-
cancelAtPeriodEnd: boolean
30-
canceledAt: string | null
31-
tier: number
32-
}
33-
34-
export interface SubscriptionLimits {
35-
creditsPerBlock: number
36-
blockDurationHours: number
37-
weeklyCreditsLimit: number
38-
}
39-
40-
export interface SubscriptionData {
41-
hasSubscription: boolean
42-
displayName?: string
43-
subscription?: SubscriptionInfo
44-
rateLimit?: SubscriptionRateLimit
45-
limits?: SubscriptionLimits
46-
}
47-
4816
export async function fetchSubscriptionData(
4917
logger: Logger = defaultLogger,
50-
): Promise<SubscriptionData> {
18+
): Promise<SubscriptionResponse> {
5119
const client = getApiClient()
52-
const response = await client.get<SubscriptionData>(
20+
const response = await client.get<SubscriptionResponse>(
5321
'/api/user/subscription',
5422
{ includeCookie: true },
5523
)

0 commit comments

Comments
 (0)