Skip to content

Commit 3a58fa3

Browse files
KevenWMarkhamclaude
andcommitted
feat: add unlock code requirement for paid mode access
Implemented a security feature that requires users to enter a special unlock code (999-0000-999) before they can access the paid service mode (credit card purchases). Key changes: - Added paidModeUnlocked boolean to ApiKeyConfig interface - Paid mode option is now grayed out and disabled by default - Added unlock code input UI with validation - Shows "🔒 Locked" badge on paid mode option when locked - Unlock status persists in localStorage - Auto-formatted unlock code input (000-0000-000 format) This provides an additional layer of control over who can access the paid service features. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
1 parent 9b9f45b commit 3a58fa3

File tree

2 files changed

+125
-10
lines changed

2 files changed

+125
-10
lines changed

.claude/settings.local.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,8 @@
1919
"Bash(wait)",
2020
"Bash(explorer.exe \"Transcript Parser v1.0.0 - Production Build\")",
2121
"Bash(npm run dev:*)",
22-
"Bash($null)"
22+
"Bash($null)",
23+
"Bash(npm run build:*)"
2324
],
2425
"deny": [],
2526
"ask": []

src/components/ApiKeySettings.tsx

Lines changed: 123 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ export interface ApiKeyConfig {
2222
ownKey?: string
2323
paidBalance?: number
2424
accessCode?: string
25+
paidModeUnlocked?: boolean
2526
}
2627

2728
interface ApiKeySettingsProps {
@@ -54,6 +55,9 @@ const VALID_CODES = [
5455
},
5556
]
5657

58+
// Special code to unlock paid mode (credit card purchases)
59+
const PAID_MODE_UNLOCK_CODE = '999-0000-999'
60+
5761
// Option 3: Load additional codes from environment variable
5862
// Set VITE_VALID_ACCESS_CODES=123-4567-890,999-8888-777,555-1234-999 in .env
5963
const getValidCodesFromEnv = (): string[] => {
@@ -82,13 +86,21 @@ export function ApiKeySettings({
8286
const [paidBalance, setPaidBalance] = useState(
8387
currentConfig?.paidBalance || 0
8488
)
89+
const [paidModeUnlocked, setPaidModeUnlocked] = useState(
90+
currentConfig?.paidModeUnlocked || false
91+
)
92+
const [unlockCode, setUnlockCode] = useState('')
93+
const [unlockStatus, setUnlockStatus] = useState<
94+
'idle' | 'valid' | 'invalid'
95+
>('idle')
8596

8697
useEffect(() => {
8798
if (currentConfig) {
8899
setMode(currentConfig.mode)
89100
setApiKey(currentConfig.ownKey || '')
90101
setAccessCode(currentConfig.accessCode || '')
91102
setPaidBalance(currentConfig.paidBalance || 0)
103+
setPaidModeUnlocked(currentConfig.paidModeUnlocked || false)
92104
}
93105
}, [currentConfig])
94106

@@ -109,6 +121,22 @@ export function ApiKeySettings({
109121
}
110122
}
111123

124+
const validateUnlockCode = (): boolean => {
125+
if (!unlockCode || unlockCode.trim().length === 0) {
126+
setUnlockStatus('invalid')
127+
return false
128+
}
129+
130+
if (unlockCode === PAID_MODE_UNLOCK_CODE) {
131+
setUnlockStatus('valid')
132+
setPaidModeUnlocked(true)
133+
return true
134+
}
135+
136+
setUnlockStatus('invalid')
137+
return false
138+
}
139+
112140
const validateAccessCode = (): boolean => {
113141
if (!accessCode || accessCode.trim().length === 0) {
114142
setValidationStatus('invalid')
@@ -270,6 +298,7 @@ export function ApiKeySettings({
270298
ownKey: mode === 'own' ? apiKey : undefined,
271299
paidBalance: mode === 'paid' ? paidBalance : undefined,
272300
accessCode: mode === 'code' ? accessCode : undefined,
301+
paidModeUnlocked: paidModeUnlocked,
273302
}
274303

275304
// Save to localStorage
@@ -287,6 +316,7 @@ export function ApiKeySettings({
287316
const config: ApiKeyConfig = {
288317
mode: 'paid',
289318
paidBalance: newBalance,
319+
paidModeUnlocked: paidModeUnlocked,
290320
}
291321
localStorage.setItem(API_KEY_STORAGE_KEY, JSON.stringify(config))
292322
onSave(config)
@@ -392,27 +422,47 @@ export function ApiKeySettings({
392422

393423
{/* Paid Service Option */}
394424
<button
395-
onClick={() => setMode('paid')}
396-
className={`p-4 border-2 rounded-lg text-left transition-all ${
397-
mode === 'paid'
398-
? 'border-emerald-500 bg-emerald-50'
399-
: 'border-gray-200 hover:border-gray-300'
425+
onClick={() => paidModeUnlocked && setMode('paid')}
426+
disabled={!paidModeUnlocked}
427+
className={`p-4 border-2 rounded-lg text-left transition-all relative ${
428+
!paidModeUnlocked
429+
? 'border-gray-300 bg-gray-100 opacity-50 cursor-not-allowed'
430+
: mode === 'paid'
431+
? 'border-emerald-500 bg-emerald-50'
432+
: 'border-gray-200 hover:border-gray-300'
400433
}`}
401434
>
435+
{!paidModeUnlocked && (
436+
<div className="absolute top-2 right-2 bg-red-100 text-red-700 text-xs px-2 py-1 rounded font-medium">
437+
🔒 Locked
438+
</div>
439+
)}
402440
<div className="flex items-start gap-3">
403441
<div
404-
className={`mt-1 ${mode === 'paid' ? 'text-emerald-600' : 'text-gray-400'}`}
442+
className={`mt-1 ${
443+
!paidModeUnlocked
444+
? 'text-gray-400'
445+
: mode === 'paid'
446+
? 'text-emerald-600'
447+
: 'text-gray-400'
448+
}`}
405449
>
406450
<CreditCard className="w-5 h-5" />
407451
</div>
408452
<div className="flex-1">
409-
<h3 className="font-semibold text-gray-900">
453+
<h3
454+
className={`font-semibold ${!paidModeUnlocked ? 'text-gray-500' : 'text-gray-900'}`}
455+
>
410456
Use Our Service
411457
</h3>
412-
<p className="text-sm text-gray-600 mt-1">
458+
<p
459+
className={`text-sm mt-1 ${!paidModeUnlocked ? 'text-gray-400' : 'text-gray-600'}`}
460+
>
413461
Pay-as-you-go pricing
414462
</p>
415-
<ul className="text-xs text-gray-500 mt-2 space-y-1">
463+
<ul
464+
className={`text-xs mt-2 space-y-1 ${!paidModeUnlocked ? 'text-gray-400' : 'text-gray-500'}`}
465+
>
416466
<li>• No API key needed</li>
417467
<li>• Monthly billing</li>
418468
<li>• Cost tracking</li>
@@ -423,6 +473,70 @@ export function ApiKeySettings({
423473
</div>
424474
</div>
425475

476+
{/* Unlock Paid Mode Section */}
477+
{!paidModeUnlocked && (
478+
<div className="space-y-3 p-4 bg-amber-50 rounded-lg border border-amber-200">
479+
<div className="flex items-start gap-3">
480+
<div className="text-amber-600">
481+
<AlertCircle className="w-5 h-5" />
482+
</div>
483+
<div className="flex-1">
484+
<h4 className="text-sm font-medium text-gray-900">
485+
Unlock Paid Service Mode
486+
</h4>
487+
<p className="text-sm text-gray-600 mt-1">
488+
Enter the unlock code to enable credit card purchases and
489+
paid service features.
490+
</p>
491+
</div>
492+
</div>
493+
<div className="space-y-2">
494+
<label
495+
htmlFor="unlockCode"
496+
className="block text-sm font-medium text-gray-700"
497+
>
498+
Unlock Code
499+
</label>
500+
<div className="flex gap-2">
501+
<input
502+
id="unlockCode"
503+
type="text"
504+
value={unlockCode}
505+
onChange={e => {
506+
setUnlockCode(formatAccessCode(e.target.value))
507+
setUnlockStatus('idle')
508+
}}
509+
placeholder="999-0000-999"
510+
maxLength={12}
511+
className="flex-1 px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-amber-500 focus:border-transparent font-mono text-lg tracking-wider"
512+
/>
513+
<Button
514+
onClick={validateUnlockCode}
515+
className="bg-amber-600 hover:bg-amber-700 text-white"
516+
>
517+
Unlock
518+
</Button>
519+
</div>
520+
{unlockStatus === 'valid' && (
521+
<div className="flex items-center gap-2 text-sm text-emerald-600">
522+
<CheckCircle2 className="w-4 h-4" />
523+
<span>
524+
Paid mode unlocked! You can now select "Use Our Service".
525+
</span>
526+
</div>
527+
)}
528+
{unlockStatus === 'invalid' && (
529+
<div className="flex items-center gap-2 text-sm text-red-600">
530+
<AlertCircle className="w-4 h-4" />
531+
<span>
532+
Invalid unlock code. Please contact support for access.
533+
</span>
534+
</div>
535+
)}
536+
</div>
537+
</div>
538+
)}
539+
426540
{/* Own API Key Configuration */}
427541
{mode === 'own' && (
428542
<div className="space-y-4 p-4 bg-blue-50 rounded-lg border border-blue-200">

0 commit comments

Comments
 (0)