@@ -22,6 +22,7 @@ export interface ApiKeyConfig {
2222 ownKey ?: string
2323 paidBalance ?: number
2424 accessCode ?: string
25+ paidModeUnlocked ?: boolean
2526}
2627
2728interface 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
5963const 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