Skip to content

Commit ce513ea

Browse files
committed
feat: Add fallbackToALaCarte server-side preference
1 parent 1f8ae74 commit ce513ea

File tree

13 files changed

+3390
-95
lines changed

13 files changed

+3390
-95
lines changed

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

Lines changed: 7 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,10 @@ import { Button } from './button'
66
import { ProgressBar } from './progress-bar'
77
import { useSubscriptionQuery } from '../hooks/use-subscription-query'
88
import { useTheme } from '../hooks/use-theme'
9+
import { useUpdatePreference } from '../hooks/use-update-preference'
910
import { useUsageQuery } from '../hooks/use-usage-query'
1011
import { WEBSITE_URL } from '../login/constants'
1112
import { useChatStore } from '../state/chat-store'
12-
import {
13-
getAlwaysUseALaCarte,
14-
setAlwaysUseALaCarte,
15-
} from '../utils/settings'
1613
import { formatResetTime } from '../utils/time-format'
1714
import { BORDER_CHARS } from '../utils/ui-constants'
1815

@@ -38,14 +35,11 @@ export const SubscriptionLimitBanner = () => {
3835
const currentTier = subscriptionData?.hasSubscription ? subscriptionData.subscription.tier : 0
3936
const canUpgrade = currentTier < maxTier
4037

41-
const [alwaysALaCarte, setAlwaysALaCarteState] = React.useState(
42-
() => getAlwaysUseALaCarte(),
43-
)
38+
const fallbackToALaCarte = subscriptionData?.fallbackToALaCarte ?? false
39+
const updatePreference = useUpdatePreference()
4440

45-
const handleToggleAlwaysALaCarte = () => {
46-
const newValue = !alwaysALaCarte
47-
setAlwaysALaCarteState(newValue)
48-
setAlwaysUseALaCarte(newValue)
41+
const handleToggleFallbackToALaCarte = () => {
42+
updatePreference.mutate({ fallbackToALaCarte: !fallbackToALaCarte })
4943
}
5044

5145
if (!subscriptionData || !rateLimit?.limited) {
@@ -138,9 +132,9 @@ export const SubscriptionLimitBanner = () => {
138132
</box>
139133

140134
{hasAlaCarteCredits && (
141-
<Button onClick={handleToggleAlwaysALaCarte}>
135+
<Button onClick={handleToggleFallbackToALaCarte}>
142136
<text style={{ fg: theme.muted }}>
143-
{alwaysALaCarte ? '[x]' : '[ ]'} always use credits if subscription limit is reached
137+
{fallbackToALaCarte ? '[x]' : '[ ]'} always use credits if subscription limit is reached
144138
</text>
145139
</Button>
146140
)}

cli/src/components/usage-banner.tsx

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { isClaudeOAuthValid } from '@codebuff/sdk'
22
import { TextAttributes } from '@opentui/core'
33
import open from 'open'
4-
import React, { useEffect, useMemo, useState } from 'react'
4+
import React, { useEffect, useMemo } from 'react'
55

66
import { BottomBanner } from './bottom-banner'
77
import { Button } from './button'
@@ -10,9 +10,9 @@ import { getActivityQueryData } from '../hooks/use-activity-query'
1010
import { useClaudeQuotaQuery } from '../hooks/use-claude-quota-query'
1111
import { useSubscriptionQuery } from '../hooks/use-subscription-query'
1212
import { useTheme } from '../hooks/use-theme'
13+
import { useUpdatePreference } from '../hooks/use-update-preference'
1314
import { usageQueryKeys, useUsageQuery } from '../hooks/use-usage-query'
1415
import { WEBSITE_URL } from '../login/constants'
15-
import { getAlwaysUseALaCarte, setAlwaysUseALaCarte } from '../utils/settings'
1616
import { useChatStore } from '../state/chat-store'
1717
import { formatResetTime, formatResetTimeLong } from '../utils/time-format'
1818
import {
@@ -122,6 +122,7 @@ export const UsageBanner = ({ showTime }: { showTime: number }) => {
122122
subscriptionInfo={subscriptionInfo}
123123
rateLimit={rateLimit}
124124
isLoading={isSubscriptionLoading}
125+
fallbackToALaCarte={activeSubscription.fallbackToALaCarte}
125126
/>
126127
)}
127128

@@ -209,21 +210,21 @@ interface SubscriptionUsageSectionProps {
209210
weeklyResetsAt: string
210211
}
211212
isLoading: boolean
213+
fallbackToALaCarte: boolean
212214
}
213215

214216
const SubscriptionUsageSection: React.FC<SubscriptionUsageSectionProps> = ({
215217
displayName,
216218
subscriptionInfo,
217219
rateLimit,
218220
isLoading,
221+
fallbackToALaCarte,
219222
}) => {
220223
const theme = useTheme()
221-
const [useALaCarte, setUseALaCarte] = useState(() => getAlwaysUseALaCarte())
224+
const updatePreference = useUpdatePreference()
222225

223-
const handleToggleALaCarte = () => {
224-
const newValue = !useALaCarte
225-
setUseALaCarte(newValue)
226-
setAlwaysUseALaCarte(newValue)
226+
const handleToggleFallbackToALaCarte = () => {
227+
updatePreference.mutate({ fallbackToALaCarte: !fallbackToALaCarte })
227228
}
228229

229230
const blockPercent = useMemo(() => {
@@ -268,11 +269,11 @@ const SubscriptionUsageSection: React.FC<SubscriptionUsageSectionProps> = ({
268269
<box style={{ flexDirection: 'row', alignItems: 'center', gap: 1, marginTop: 1 }}>
269270
<text style={{ fg: theme.muted }}>When limit reached:</text>
270271
<text style={{ fg: theme.muted }}>
271-
{useALaCarte ? 'spend credits' : 'pause'}
272+
{fallbackToALaCarte ? 'spend credits' : 'pause'}
272273
</text>
273-
<Button onClick={handleToggleALaCarte}>
274+
<Button onClick={handleToggleFallbackToALaCarte}>
274275
<text style={{ fg: theme.muted, attributes: TextAttributes.UNDERLINE }}>
275-
[{useALaCarte ? 'switch to pause' : 'switch to spend credits'}]
276+
[{fallbackToALaCarte ? 'switch to pause' : 'switch to spend credits'}]
276277
</text>
277278
</Button>
278279
</box>
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
import { useMutation, useQueryClient } from '@tanstack/react-query'
2+
3+
import { subscriptionQueryKeys } from './use-subscription-query'
4+
import { getApiClient } from '../utils/codebuff-api'
5+
import { logger } from '../utils/logger'
6+
7+
import type { SubscriptionResponse } from '@codebuff/common/types/subscription'
8+
9+
interface UpdatePreferenceParams {
10+
fallbackToALaCarte?: boolean
11+
}
12+
13+
export function useUpdatePreference() {
14+
const queryClient = useQueryClient()
15+
16+
return useMutation({
17+
mutationFn: async (params: UpdatePreferenceParams) => {
18+
const client = getApiClient()
19+
const response = await client.patch('/api/user/preferences', {
20+
body: params,
21+
includeCookie: true,
22+
})
23+
24+
if (!response.ok) {
25+
throw new Error('Failed to update preference')
26+
}
27+
28+
return params
29+
},
30+
onMutate: async (newParams) => {
31+
// Cancel any outgoing refetches
32+
await queryClient.cancelQueries({ queryKey: subscriptionQueryKeys.current() })
33+
34+
// Snapshot the previous value
35+
const previousData = queryClient.getQueryData<SubscriptionResponse>(
36+
subscriptionQueryKeys.current()
37+
)
38+
39+
// Optimistically update to the new value
40+
if (previousData && newParams.fallbackToALaCarte !== undefined) {
41+
queryClient.setQueryData<SubscriptionResponse>(
42+
subscriptionQueryKeys.current(),
43+
{ ...previousData, fallbackToALaCarte: newParams.fallbackToALaCarte }
44+
)
45+
}
46+
47+
return { previousData }
48+
},
49+
onError: (err, _newParams, context) => {
50+
// Rollback to previous value on error
51+
if (context?.previousData) {
52+
queryClient.setQueryData(subscriptionQueryKeys.current(), context.previousData)
53+
}
54+
logger.error({ err }, 'Failed to update preference')
55+
},
56+
onSettled: () => {
57+
// Refetch after mutation
58+
queryClient.invalidateQueries({ queryKey: subscriptionQueryKeys.current() })
59+
},
60+
})
61+
}

cli/src/utils/settings.ts

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,10 @@ const DEFAULT_SETTINGS: Settings = {
2020
export interface Settings {
2121
mode?: AgentMode
2222
adsEnabled?: boolean
23+
/** @deprecated Use server-side fallbackToALaCarte setting instead */
2324
alwaysUseALaCarte?: boolean
25+
/** @deprecated Use server-side fallbackToALaCarte setting instead */
26+
fallbackToALaCarte?: boolean
2427
}
2528

2629
/**
@@ -93,11 +96,16 @@ const validateSettings = (parsed: unknown): Settings => {
9396
settings.adsEnabled = obj.adsEnabled
9497
}
9598

96-
// Validate alwaysUseALaCarte
99+
// Validate alwaysUseALaCarte (legacy)
97100
if (typeof obj.alwaysUseALaCarte === 'boolean') {
98101
settings.alwaysUseALaCarte = obj.alwaysUseALaCarte
99102
}
100103

104+
// Validate fallbackToALaCarte (legacy)
105+
if (typeof obj.fallbackToALaCarte === 'boolean') {
106+
settings.fallbackToALaCarte = obj.fallbackToALaCarte
107+
}
108+
101109
return settings
102110
}
103111

@@ -143,15 +151,17 @@ export const saveModePreference = (mode: AgentMode): void => {
143151

144152
/**
145153
* Load the "always use a-la-carte" preference
154+
* @deprecated Use server-side fallbackToALaCarte setting via useSubscriptionQuery instead
146155
*/
147156
export const getAlwaysUseALaCarte = (): boolean => {
148157
const settings = loadSettings()
149-
return settings.alwaysUseALaCarte ?? false
158+
return settings.fallbackToALaCarte ?? settings.alwaysUseALaCarte ?? false
150159
}
151160

152161
/**
153162
* Save the "always use a-la-carte" preference
163+
* @deprecated Use server-side fallbackToALaCarte setting via useUpdatePreference instead
154164
*/
155165
export const setAlwaysUseALaCarte = (value: boolean): void => {
156-
saveSettings({ alwaysUseALaCarte: value })
166+
saveSettings({ fallbackToALaCarte: value })
157167
}

common/src/types/subscription.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,8 @@ export interface SubscriptionLimits {
4040
*/
4141
export interface NoSubscriptionResponse {
4242
hasSubscription: false
43+
/** Whether user prefers to fallback to a-la-carte credits when subscription limits are reached */
44+
fallbackToALaCarte: boolean
4345
}
4446

4547
/**
@@ -53,6 +55,8 @@ export interface ActiveSubscriptionResponse {
5355
rateLimit: SubscriptionRateLimit
5456
limits: SubscriptionLimits
5557
billingPortalUrl?: string
58+
/** Whether user prefers to fallback to a-la-carte credits when subscription limits are reached */
59+
fallbackToALaCarte: boolean
5660
}
5761

5862
/**
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
ALTER TABLE "user" ADD COLUMN "fallback_to_a_la_carte" boolean DEFAULT false NOT NULL;

0 commit comments

Comments
 (0)