Skip to content

Commit ed33ed3

Browse files
committed
web: simplify buy credits panel. no custom purchase amount
1 parent 52135d9 commit ed33ed3

File tree

5 files changed

+50
-171
lines changed

5 files changed

+50
-171
lines changed

web/src/app/payment-success/page.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -28,12 +28,12 @@ function PaymentSuccessContent() {
2828
} = useAutoTopup()
2929

3030
const enableMinimumAutoTopup = async () => {
31-
const { MIN_THRESHOLD_CREDITS, MIN_TOPUP_DOLLARS } = AUTO_TOPUP_CONSTANTS
31+
const { MIN_THRESHOLD_CREDITS, DEFAULT_TOPUP_DOLLARS } =
32+
AUTO_TOPUP_CONSTANTS
3233

33-
// Enable auto top-up with minimum values
3434
await handleToggleAutoTopup(true)
3535
handleThresholdChange(MIN_THRESHOLD_CREDITS)
36-
handleTopUpAmountChange(MIN_TOPUP_DOLLARS)
36+
handleTopUpAmountChange(DEFAULT_TOPUP_DOLLARS)
3737
}
3838

3939
useEffect(() => {

web/src/app/profile/components/subscription-section.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,7 @@ function SubscriptionActive({ data }: { data: ActiveSubscriptionResponse }) {
9696
<CardHeader className="pb-5">
9797
<CardTitle className="flex items-baseline gap-2 text-lg">
9898
<span>💪</span>
99-
{SUBSCRIPTION_DISPLAY_NAME}
99+
{SUBSCRIPTION_DISPLAY_NAME} Subscription
100100
<span className="text-sm font-normal text-muted-foreground">
101101
${subscription.tier}/mo
102102
</span>
@@ -164,7 +164,7 @@ function SubscriptionActive({ data }: { data: ActiveSubscriptionResponse }) {
164164
disabled={updatePreferenceMutation.isPending}
165165
/>
166166
<Label htmlFor="always-use-credits" className="text-sm cursor-pointer">
167-
Use a-la-carte credits when limit is reached
167+
Automatically use credits when limit is reached
168168
</Label>
169169
</div>
170170
</CardContent>
Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
export const AUTO_TOPUP_CONSTANTS = {
22
MIN_THRESHOLD_CREDITS: 150,
33
MAX_THRESHOLD_CREDITS: 1000,
4-
MIN_TOPUP_DOLLARS: 5.0,
4+
MIN_TOPUP_DOLLARS: 10.0,
5+
DEFAULT_TOPUP_DOLLARS: 20.0,
56
MAX_TOPUP_DOLLARS: 100.0,
67
CENTS_PER_CREDIT: 1,
78
} as const
Lines changed: 35 additions & 158 deletions
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,17 @@
1-
import { convertCreditsToUsdCents } from '@codebuff/common/util/currency'
2-
import { pluralize } from '@codebuff/common/util/string'
31
import { Loader2 as Loader } from 'lucide-react'
42
import { useState } from 'react'
53

64
import { Button } from '@/components/ui/button'
7-
import { Input } from '@/components/ui/input'
8-
import { Label } from '@/components/ui/label'
9-
import { NeonGradientButton } from '@/components/ui/neon-gradient-button'
10-
import { formatDollars } from '@/lib/currency'
5+
import { dollarsToCredits } from '@/lib/currency'
116
import { cn } from '@/lib/utils'
127

13-
// Individual user credit options (starting from $10)
14-
export const CREDIT_OPTIONS = [1000, 2500, 5000, 10000] as const
15-
export const CENTS_PER_CREDIT = 1
16-
const MIN_CREDITS = 500
17-
const MAX_CREDITS = 100000
18-
19-
// Organization credit options (starting from $100)
20-
export const ORG_CREDIT_OPTIONS = [10000, 25000, 50000, 100000] as const
21-
const MIN_CREDITS_ORG = 5000
22-
const MAX_CREDITS_ORG = 1000000
8+
const DOLLAR_OPTIONS = [10, 25, 50, 100] as const
9+
const ORG_DOLLAR_OPTIONS = [50, 100, 250, 1000] as const
2310

2411
export interface CreditPurchaseSectionProps {
2512
onPurchase: (credits: number) => void
2613
onSaveAutoTopupSettings?: () => Promise<boolean>
2714
isAutoTopupEnabled?: boolean
28-
isAutoTopupPending?: boolean
2915
isPending?: boolean
3016
isPurchasePending: boolean
3117
isOrganization?: boolean
@@ -35,166 +21,57 @@ export function CreditPurchaseSection({
3521
onPurchase,
3622
onSaveAutoTopupSettings,
3723
isAutoTopupEnabled,
38-
isAutoTopupPending,
3924
isPending,
4025
isPurchasePending,
4126
isOrganization = false,
4227
}: CreditPurchaseSectionProps) {
43-
const [selectedCredits, setSelectedCredits] = useState<number | null>(null)
44-
const [customCredits, setCustomCredits] = useState<string>('')
45-
const [customError, setCustomError] = useState<string>('')
4628
const [cooldownActive, setCooldownActive] = useState(false)
29+
const [purchasingDollars, setPurchasingDollars] = useState<number | null>(
30+
null,
31+
)
4732

48-
// Use organization-specific options if isOrganization is true
49-
const creditOptions = isOrganization ? ORG_CREDIT_OPTIONS : CREDIT_OPTIONS
50-
const minCredits = isOrganization ? MIN_CREDITS_ORG : MIN_CREDITS
51-
const maxCredits = isOrganization ? MAX_CREDITS_ORG : MAX_CREDITS
33+
const dollarOptions = isOrganization ? ORG_DOLLAR_OPTIONS : DOLLAR_OPTIONS
34+
const isDisabled = isPending || isPurchasePending || cooldownActive
5235

53-
const handlePurchaseClick = async () => {
54-
const credits = selectedCredits || parseInt(customCredits)
55-
if (!credits || isPurchasePending || isPending || cooldownActive) return
36+
const handlePurchase = async (dollars: number) => {
37+
if (isDisabled) return
5638

5739
let canProceed = true
5840
if (isAutoTopupEnabled && onSaveAutoTopupSettings) {
5941
canProceed = await onSaveAutoTopupSettings()
6042
}
6143

6244
if (canProceed) {
45+
setPurchasingDollars(dollars)
6346
setCooldownActive(true)
64-
setTimeout(() => setCooldownActive(false), 3000) // 3 second cooldown
65-
onPurchase(credits)
47+
setTimeout(() => {
48+
setCooldownActive(false)
49+
setPurchasingDollars(null)
50+
}, 3000)
51+
onPurchase(dollarsToCredits(dollars))
6652
}
6753
}
6854

69-
const handleCreditSelection = (credits: number) => {
70-
setSelectedCredits((currentSelected) =>
71-
currentSelected === credits ? null : credits,
72-
)
73-
setCustomCredits('')
74-
setCustomError('')
75-
}
76-
77-
const handleCustomCreditsChange = (value: string) => {
78-
setCustomCredits(value)
79-
setSelectedCredits(null)
80-
81-
if (!value) {
82-
setCustomError('')
83-
return
84-
}
85-
86-
const numCredits = parseInt(value)
87-
if (isNaN(numCredits)) {
88-
setCustomError('Please enter a valid number')
89-
} else if (numCredits < minCredits) {
90-
setCustomError(`Minimum ${pluralize(minCredits, 'credit')}`)
91-
} else if (numCredits > maxCredits) {
92-
setCustomError(`Maximum ${pluralize(maxCredits, 'credit')}`)
93-
} else {
94-
setCustomError('')
95-
}
96-
}
97-
98-
const isValid = selectedCredits || (customCredits && !customError)
99-
const effectiveCredits =
100-
selectedCredits ||
101-
(customCredits && !customError ? parseInt(customCredits) : null)
102-
const costInCents = effectiveCredits
103-
? convertCreditsToUsdCents(effectiveCredits, CENTS_PER_CREDIT)
104-
: 0
105-
106-
const costInDollars = formatDollars(costInCents)
107-
10855
return (
109-
<div className="space-y-6">
110-
<div className="grid grid-cols-2 sm:grid-cols-4 gap-3">
111-
{creditOptions.map((credits) => {
112-
const optionCostInCents = convertCreditsToUsdCents(
113-
credits,
114-
CENTS_PER_CREDIT,
115-
)
116-
const optionCostInDollars = formatDollars(optionCostInCents)
117-
118-
return (
119-
<Button
120-
key={credits}
121-
variant="outline"
122-
onClick={() => handleCreditSelection(credits)}
123-
className={cn(
124-
'flex flex-col p-4 h-auto gap-1 transition-colors',
125-
selectedCredits === credits
126-
? 'border-primary bg-accent'
127-
: 'hover:bg-accent/50',
128-
)}
129-
disabled={isPending || isPurchasePending || cooldownActive}
130-
>
131-
<span className="text-lg font-semibold">
132-
{credits.toLocaleString()}
133-
</span>
134-
<span className="text-sm text-muted-foreground">
135-
${optionCostInDollars}
136-
</span>
137-
</Button>
138-
)
139-
})}
140-
</div>
141-
142-
<div className="flex flex-col md:flex-row gap-4 items-start md:items-end">
143-
<div className="w-full flex-1 space-y-2">
144-
<Label htmlFor="custom-credits">Or enter a custom amount:</Label>
145-
<div>
146-
<div className="flex flex-col md:flex-row gap-4 items-start">
147-
<div className="w-full flex-1">
148-
<Input
149-
id="custom-credits"
150-
type="number"
151-
min={minCredits}
152-
max={maxCredits}
153-
value={customCredits}
154-
onChange={(e) => handleCustomCreditsChange(e.target.value)}
155-
placeholder={`${pluralize(minCredits, 'credit')} - ${pluralize(maxCredits, 'credit')}`}
156-
className={cn(customError && 'border-destructive')}
157-
disabled={cooldownActive}
158-
/>
159-
{customError && (
160-
<p className="text-xs text-destructive mt-2 pl-1">
161-
{customError}
162-
</p>
163-
)}
164-
{customCredits && !customError && (
165-
<p className="text-sm text-muted-foreground mt-2 pl-1">
166-
We'll charge you ${costInDollars}
167-
</p>
168-
)}
169-
</div>
170-
171-
<NeonGradientButton
172-
onClick={handlePurchaseClick}
173-
disabled={
174-
!isValid || isPending || isPurchasePending || cooldownActive
175-
}
176-
className={cn(
177-
'w-full md:w-auto transition-opacity min-w-[120px]',
178-
(!isValid ||
179-
isPending ||
180-
isPurchasePending ||
181-
cooldownActive) &&
182-
'opacity-50',
183-
)}
184-
neonColors={{
185-
firstColor: '#4F46E5',
186-
secondColor: '#06B6D4',
187-
}}
188-
>
189-
{isPurchasePending ? (
190-
<Loader className="mr-2 size-4 animate-spin" />
191-
) : null}
192-
Buy Credits
193-
</NeonGradientButton>
194-
</div>
195-
</div>
196-
</div>
197-
</div>
56+
<div className="grid grid-cols-2 sm:grid-cols-4 gap-3">
57+
{dollarOptions.map((dollars) => (
58+
<Button
59+
key={dollars}
60+
variant="outline"
61+
onClick={() => handlePurchase(dollars)}
62+
className={cn(
63+
'flex flex-col p-4 h-auto transition-all',
64+
'hover:bg-accent/50 hover:border-primary',
65+
)}
66+
disabled={isDisabled}
67+
>
68+
{isPurchasePending && purchasingDollars === dollars ? (
69+
<Loader className="size-5 animate-spin" />
70+
) : (
71+
<span className="text-xl font-bold">${dollars}</span>
72+
)}
73+
</Button>
74+
))}
19875
</div>
19976
)
20077
}

web/src/hooks/use-auto-topup.ts

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ const {
1414
MIN_THRESHOLD_CREDITS,
1515
MAX_THRESHOLD_CREDITS,
1616
MIN_TOPUP_DOLLARS,
17+
DEFAULT_TOPUP_DOLLARS,
1718
MAX_TOPUP_DOLLARS,
1819
CENTS_PER_CREDIT,
1920
} = AUTO_TOPUP_CONSTANTS
@@ -23,7 +24,7 @@ export function useAutoTopup(): AutoTopupState {
2324
const [isEnabled, setIsEnabled] = useState(false)
2425
const [threshold, setThreshold] = useState<number>(MIN_THRESHOLD_CREDITS)
2526
const [topUpAmountDollars, setTopUpAmountDollars] =
26-
useState<number>(MIN_TOPUP_DOLLARS)
27+
useState<number>(DEFAULT_TOPUP_DOLLARS)
2728
const isInitialLoad = useRef(true)
2829
const pendingSettings = useRef<{
2930
threshold: number
@@ -40,7 +41,7 @@ export function useAutoTopup(): AutoTopupState {
4041
const data = await response.json()
4142
const thresholdCredits =
4243
data.auto_topup_threshold ?? MIN_THRESHOLD_CREDITS
43-
const topUpAmount = data.auto_topup_amount ?? MIN_TOPUP_DOLLARS * 100
44+
const topUpAmount = data.auto_topup_amount ?? DEFAULT_TOPUP_DOLLARS * 100
4445
const topUpDollars = topUpAmount / 100
4546

4647
return {
@@ -52,7 +53,7 @@ export function useAutoTopup(): AutoTopupState {
5253
MAX_THRESHOLD_CREDITS,
5354
),
5455
initialTopUpDollars: clamp(
55-
topUpDollars > 0 ? topUpDollars : MIN_TOPUP_DOLLARS,
56+
topUpDollars > 0 ? topUpDollars : DEFAULT_TOPUP_DOLLARS,
5657
MIN_TOPUP_DOLLARS,
5758
MAX_TOPUP_DOLLARS,
5859
),
@@ -76,7 +77,7 @@ export function useAutoTopup(): AutoTopupState {
7677
setIsEnabled(userProfile.auto_topup_enabled ?? false)
7778
setThreshold(userProfile.auto_topup_threshold ?? MIN_THRESHOLD_CREDITS)
7879
setTopUpAmountDollars(
79-
userProfile.initialTopUpDollars ?? MIN_TOPUP_DOLLARS,
80+
userProfile.initialTopUpDollars ?? DEFAULT_TOPUP_DOLLARS,
8081
)
8182
setTimeout(() => {
8283
isInitialLoad.current = false
@@ -190,13 +191,13 @@ export function useAutoTopup(): AutoTopupState {
190191
initialTopUpDollars:
191192
savedEnabled && savedAmountCents
192193
? savedAmountCents / 100
193-
: MIN_TOPUP_DOLLARS,
194+
: DEFAULT_TOPUP_DOLLARS,
194195
}
195196

196197
setIsEnabled(updatedData.auto_topup_enabled ?? false)
197198
setThreshold(updatedData.auto_topup_threshold ?? MIN_THRESHOLD_CREDITS)
198199
setTopUpAmountDollars(
199-
updatedData.initialTopUpDollars ?? MIN_TOPUP_DOLLARS,
200+
updatedData.initialTopUpDollars ?? DEFAULT_TOPUP_DOLLARS,
200201
)
201202

202203
return updatedData
@@ -214,7 +215,7 @@ export function useAutoTopup(): AutoTopupState {
214215
setIsEnabled(userProfile.auto_topup_enabled ?? false)
215216
setThreshold(userProfile.auto_topup_threshold ?? MIN_THRESHOLD_CREDITS)
216217
setTopUpAmountDollars(
217-
userProfile.initialTopUpDollars ?? MIN_TOPUP_DOLLARS,
218+
userProfile.initialTopUpDollars ?? DEFAULT_TOPUP_DOLLARS,
218219
)
219220
}
220221
pendingSettings.current = null

0 commit comments

Comments
 (0)