Skip to content

Commit 4de78bc

Browse files
jontsaiclaude
andauthored
Add AI credits purchase feature with Stripe Payment Links (#1)
- Add 6 credit tier options ($5-$50) with bonus credits for larger purchases - Create CreditsModal component with pricing grid - Add "Buy Credits" button to header - Configure Stripe Payment Link URLs for one-time purchases 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude <noreply@anthropic.com>
1 parent a694ed8 commit 4de78bc

File tree

6 files changed

+223
-3
lines changed

6 files changed

+223
-3
lines changed

src/components/CreditsModal.tsx

Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
import React from 'react'
2+
import { STRIPE_CREDIT_URLS } from '@/config/stripe'
3+
import { CREDIT_TIERS } from '@/config/credits'
4+
5+
interface CreditsModalProps {
6+
isOpen: boolean
7+
onClose: () => void
8+
}
9+
10+
const CreditsModal: React.FC<CreditsModalProps> = ({ isOpen, onClose }) => {
11+
if (!isOpen) return null
12+
13+
const handlePurchase = (checkoutKey: keyof typeof STRIPE_CREDIT_URLS) => {
14+
const url = STRIPE_CREDIT_URLS[checkoutKey]
15+
window.open(url, '_blank', 'noopener,noreferrer')
16+
}
17+
18+
return (
19+
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 p-4">
20+
<div className="bg-white dark:bg-gray-900 rounded-lg shadow-2xl max-w-4xl w-full p-8 fade-in max-h-[90vh] overflow-y-auto">
21+
{/* Header */}
22+
<div className="flex items-center justify-between mb-6">
23+
<div>
24+
<h2 className="text-3xl font-mono font-bold text-electric-teal mb-2">
25+
Buy AI Credits
26+
</h2>
27+
<p className="text-gray-600 dark:text-gray-400">
28+
Top up your credits for code generation and AI assistance
29+
</p>
30+
</div>
31+
<button
32+
onClick={onClose}
33+
className="text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200 text-2xl"
34+
aria-label="Close"
35+
>
36+
37+
</button>
38+
</div>
39+
40+
{/* Credit Tiers */}
41+
<div className="grid md:grid-cols-3 gap-4 mb-6">
42+
{CREDIT_TIERS.map((tier) => (
43+
<div
44+
key={tier.checkoutKey}
45+
className={`relative border-2 rounded-lg p-6 transition-all ${
46+
tier.popular
47+
? 'border-electric-teal shadow-lg scale-105'
48+
: 'border-gray-200 dark:border-gray-700'
49+
}`}
50+
>
51+
{/* Popular Badge */}
52+
{tier.popular && (
53+
<div className="absolute -top-3 left-1/2 transform -translate-x-1/2">
54+
<span className="bg-electric-teal text-space-black px-4 py-1 rounded-full text-xs font-mono font-bold whitespace-nowrap">
55+
BEST VALUE
56+
</span>
57+
</div>
58+
)}
59+
60+
{/* Credits Amount */}
61+
<div className="text-center mb-4">
62+
<div className="text-4xl font-mono font-bold text-electric-teal mb-1">
63+
{tier.credits.toLocaleString()}
64+
</div>
65+
<div className="text-sm text-gray-600 dark:text-gray-400">
66+
credits
67+
</div>
68+
{tier.bonus > 0 && (
69+
<div className="mt-2 text-sm text-cyber-violet font-semibold">
70+
+{tier.bonus} bonus credits!
71+
</div>
72+
)}
73+
</div>
74+
75+
{/* Price */}
76+
<div className="text-center mb-4">
77+
<span className="text-3xl font-bold">${tier.price}</span>
78+
<span className="text-gray-500 dark:text-gray-400 ml-1 text-sm">
79+
one-time
80+
</span>
81+
</div>
82+
83+
{/* Value indicator */}
84+
<div className="text-center text-xs text-gray-500 dark:text-gray-400 mb-4">
85+
${(tier.price / tier.credits * 100).toFixed(1)}¢ per credit
86+
</div>
87+
88+
{/* Purchase Button */}
89+
<button
90+
onClick={() => handlePurchase(tier.checkoutKey)}
91+
className={`w-full py-3 rounded-lg font-mono font-semibold transition-colors ${
92+
tier.popular
93+
? 'bg-electric-teal text-space-black hover:bg-cyber-violet hover:text-white'
94+
: 'bg-gray-200 dark:bg-gray-800 text-gray-900 dark:text-white hover:bg-electric-teal hover:text-space-black'
95+
}`}
96+
>
97+
Buy Now
98+
</button>
99+
</div>
100+
))}
101+
</div>
102+
103+
{/* Footer Info */}
104+
<div className="border-t border-gray-200 dark:border-gray-700 pt-6">
105+
<div className="grid md:grid-cols-3 gap-4 text-xs text-gray-600 dark:text-gray-400">
106+
<div>
107+
<p className="font-semibold mb-1">Do credits expire?</p>
108+
<p>No, your credits never expire. Use them whenever you need.</p>
109+
</div>
110+
<div>
111+
<p className="font-semibold mb-1">Secure payment</p>
112+
<p>All payments are securely processed by Stripe.</p>
113+
</div>
114+
<div>
115+
<p className="font-semibold mb-1">Need more?</p>
116+
<p>Consider a subscription plan for unlimited access.</p>
117+
</div>
118+
</div>
119+
</div>
120+
</div>
121+
</div>
122+
)
123+
}
124+
125+
export default CreditsModal

src/components/Header.tsx

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,10 @@ import { useTheme } from '@/lib/useTheme'
33

44
interface HeaderProps {
55
onUpgradeClick?: () => void
6+
onBuyCreditsClick?: () => void
67
}
78

8-
const Header: React.FC<HeaderProps> = ({ onUpgradeClick }) => {
9+
const Header: React.FC<HeaderProps> = ({ onUpgradeClick, onBuyCreditsClick }) => {
910
const { theme, toggleTheme, mounted } = useTheme()
1011

1112
return (
@@ -28,8 +29,16 @@ const Header: React.FC<HeaderProps> = ({ onUpgradeClick }) => {
2829
</p>
2930
</div>
3031

31-
{/* Right Side - Upgrade Button + Theme Toggle */}
32+
{/* Right Side - Upgrade Button + Buy Credits + Theme Toggle */}
3233
<div className="flex items-center space-x-3">
34+
{onBuyCreditsClick && (
35+
<button
36+
onClick={onBuyCreditsClick}
37+
className="px-4 py-2 border-2 border-electric-teal text-electric-teal font-mono font-semibold rounded-lg hover:bg-electric-teal hover:text-space-black transition-colors duration-200"
38+
>
39+
Buy Credits
40+
</button>
41+
)}
3342
{onUpgradeClick && (
3443
<button
3544
onClick={onUpgradeClick}

src/config/credits.json

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
[
2+
{
3+
"credits": 50,
4+
"price": 5,
5+
"bonus": 0,
6+
"popular": false,
7+
"checkoutKey": "credits_50"
8+
},
9+
{
10+
"credits": 120,
11+
"price": 10,
12+
"bonus": 20,
13+
"popular": false,
14+
"checkoutKey": "credits_120"
15+
},
16+
{
17+
"credits": 200,
18+
"price": 15,
19+
"bonus": 50,
20+
"popular": false,
21+
"checkoutKey": "credits_200"
22+
},
23+
{
24+
"credits": 300,
25+
"price": 20,
26+
"bonus": 100,
27+
"popular": true,
28+
"checkoutKey": "credits_300"
29+
},
30+
{
31+
"credits": 500,
32+
"price": 25,
33+
"bonus": 250,
34+
"popular": false,
35+
"checkoutKey": "credits_500"
36+
},
37+
{
38+
"credits": 1200,
39+
"price": 50,
40+
"bonus": 600,
41+
"popular": false,
42+
"checkoutKey": "credits_1200"
43+
}
44+
]

src/config/credits.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
/**
2+
* Hexa Credits Configuration
3+
*
4+
* Single source of truth: src/config/credits.json
5+
* Edit the JSON file to update credit packages.
6+
*/
7+
8+
import creditsData from './credits.json'
9+
import { StripeCreditTier } from './stripe'
10+
11+
export interface CreditTier {
12+
credits: number
13+
price: number
14+
bonus: number
15+
popular: boolean
16+
checkoutKey: StripeCreditTier
17+
}
18+
19+
// Import from JSON (single source of truth)
20+
export const CREDIT_TIERS = creditsData as CreditTier[]

src/config/stripe.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,5 +16,15 @@ export const STRIPE_CHECKOUT_URLS = {
1616
business: 'https://buy.stripe.com/5kQdRbfmr4T9f9i1E12Ry02',
1717
} as const
1818

19+
export const STRIPE_CREDIT_URLS = {
20+
credits_50: 'https://buy.stripe.com/28EbJ32zF4T98KU6Yl2Ry03',
21+
credits_120: 'https://buy.stripe.com/4gMfZjfmrdpFe5ebeB2Ry04',
22+
credits_200: 'https://buy.stripe.com/9B6dRb0rx3P58KU5Uh2Ry05',
23+
credits_300: 'https://buy.stripe.com/14A7sN6PV5Xd9OYeqN2Ry06',
24+
credits_500: 'https://buy.stripe.com/7sY7sN1vBbhx1is4Qd2Ry07',
25+
credits_1200: 'https://buy.stripe.com/7sY28tb6bgBRf9i1E12Ry08',
26+
} as const
27+
1928
// Type-safe access
2029
export type StripeTier = keyof typeof STRIPE_CHECKOUT_URLS
30+
export type StripeCreditTier = keyof typeof STRIPE_CREDIT_URLS

src/pages/index.tsx

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import ChatPanel from '@/components/ChatPanel'
66
import CodeEditor from '@/components/CodeEditor'
77
import ProviderSwitcher from '@/components/ProviderSwitcher'
88
import PricingModal from '@/components/PricingModal'
9+
import CreditsModal from '@/components/CreditsModal'
910
import { useTheme } from '@/lib/useTheme'
1011

1112
export default function Home() {
@@ -14,6 +15,7 @@ export default function Home() {
1415
const [language, setLanguage] = useState('python')
1516
const [showCommandPalette, setShowCommandPalette] = useState(false)
1617
const [showPricingModal, setShowPricingModal] = useState(false)
18+
const [showCreditsModal, setShowCreditsModal] = useState(false)
1719

1820
useEffect(() => {
1921
// Keyboard shortcut for command palette (Cmd+K or Ctrl+K)
@@ -26,6 +28,7 @@ export default function Home() {
2628
if (e.key === 'Escape') {
2729
setShowCommandPalette(false)
2830
setShowPricingModal(false)
31+
setShowCreditsModal(false)
2932
}
3033
}
3134

@@ -47,7 +50,10 @@ export default function Home() {
4750
</Head>
4851

4952
<div className="flex flex-col h-screen">
50-
<Header onUpgradeClick={() => setShowPricingModal(true)} />
53+
<Header
54+
onUpgradeClick={() => setShowPricingModal(true)}
55+
onBuyCreditsClick={() => setShowCreditsModal(true)}
56+
/>
5157

5258
<main className="flex-1 overflow-hidden">
5359
<div className="h-full flex flex-col lg:flex-row">
@@ -141,6 +147,12 @@ export default function Home() {
141147
isOpen={showPricingModal}
142148
onClose={() => setShowPricingModal(false)}
143149
/>
150+
151+
{/* Credits Modal */}
152+
<CreditsModal
153+
isOpen={showCreditsModal}
154+
onClose={() => setShowCreditsModal(false)}
155+
/>
144156
</div>
145157
</>
146158
)

0 commit comments

Comments
 (0)