@@ -12,14 +12,16 @@ import {
1212 CreditCard ,
1313 CheckCircle2 ,
1414 AlertCircle ,
15+ Ticket ,
1516} from 'lucide-react'
1617import { PaymentModal } from './PaymentModal'
1718import { GoogleGenAI } from '@google/genai'
1819
1920export interface ApiKeyConfig {
20- mode : 'own' | 'paid'
21+ mode : 'own' | 'paid' | 'code'
2122 ownKey ?: string
2223 paidBalance ?: number
24+ accessCode ?: string
2325}
2426
2527interface ApiKeySettingsProps {
@@ -30,15 +32,19 @@ interface ApiKeySettingsProps {
3032}
3133
3234const API_KEY_STORAGE_KEY = 'gemini_api_config'
35+ const CODE_PATTERN = / ^ \d { 3 } - \d { 4 } - \d { 3 } $ / // Format: 000-0000-000
3336
3437export function ApiKeySettings ( {
3538 isOpen,
3639 onClose,
3740 onSave,
3841 currentConfig,
3942} : ApiKeySettingsProps ) {
40- const [ mode , setMode ] = useState < 'own' | 'paid' > ( currentConfig ?. mode || 'own' )
43+ const [ mode , setMode ] = useState < 'own' | 'paid' | 'code' > (
44+ currentConfig ?. mode || 'own'
45+ )
4146 const [ apiKey , setApiKey ] = useState ( currentConfig ?. ownKey || '' )
47+ const [ accessCode , setAccessCode ] = useState ( currentConfig ?. accessCode || '' )
4248 const [ isValidating , setIsValidating ] = useState ( false )
4349 const [ validationStatus , setValidationStatus ] = useState <
4450 'idle' | 'valid' | 'invalid'
@@ -53,10 +59,48 @@ export function ApiKeySettings({
5359 if ( currentConfig ) {
5460 setMode ( currentConfig . mode )
5561 setApiKey ( currentConfig . ownKey || '' )
62+ setAccessCode ( currentConfig . accessCode || '' )
5663 setPaidBalance ( currentConfig . paidBalance || 0 )
5764 }
5865 } , [ currentConfig ] )
5966
67+ const formatAccessCode = ( value : string ) : string => {
68+ // Remove all non-digits
69+ const digits = value . replace ( / \D / g, '' )
70+
71+ // Limit to 10 digits
72+ const limited = digits . slice ( 0 , 10 )
73+
74+ // Add dashes at positions 3 and 7 (after 3 digits and after 4 more digits)
75+ if ( limited . length <= 3 ) {
76+ return limited
77+ } else if ( limited . length <= 7 ) {
78+ return `${ limited . slice ( 0 , 3 ) } -${ limited . slice ( 3 ) } `
79+ } else {
80+ return `${ limited . slice ( 0 , 3 ) } -${ limited . slice ( 3 , 7 ) } -${ limited . slice ( 7 ) } `
81+ }
82+ }
83+
84+ const validateAccessCode = ( ) : boolean => {
85+ if ( ! accessCode || accessCode . trim ( ) . length === 0 ) {
86+ setValidationStatus ( 'invalid' )
87+ setValidationMessage ( 'Please enter an access code' )
88+ return false
89+ }
90+
91+ if ( ! CODE_PATTERN . test ( accessCode ) ) {
92+ setValidationStatus ( 'invalid' )
93+ setValidationMessage ( 'Invalid code format. Use format: 000-0000-000' )
94+ return false
95+ }
96+
97+ // Code format is valid - mark as valid
98+ // In a production app, you would verify this code against a database
99+ setValidationStatus ( 'valid' )
100+ setValidationMessage ( 'Access code format is valid!' )
101+ return true
102+ }
103+
60104 const validateApiKey = async ( ) => {
61105 if ( ! apiKey || apiKey . trim ( ) . length === 0 ) {
62106 setValidationStatus ( 'invalid' )
@@ -152,10 +196,18 @@ export function ApiKeySettings({
152196 }
153197 }
154198
199+ if ( mode === 'code' ) {
200+ const isValid = validateAccessCode ( )
201+ if ( ! isValid ) {
202+ return
203+ }
204+ }
205+
155206 const config : ApiKeyConfig = {
156207 mode,
157208 ownKey : mode === 'own' ? apiKey : undefined ,
158209 paidBalance : mode === 'paid' ? paidBalance : undefined ,
210+ accessCode : mode === 'code' ? accessCode : undefined ,
159211 }
160212
161213 // Save to localStorage
@@ -213,7 +265,7 @@ export function ApiKeySettings({
213265 < label className = "text-sm font-medium text-gray-700" >
214266 Access Mode
215267 </ label >
216- < div className = "grid grid-cols-1 md:grid-cols-2 gap-4" >
268+ < div className = "grid grid-cols-1 md:grid-cols-3 gap-4" >
217269 { /* Own API Key Option */ }
218270 < button
219271 onClick = { ( ) => setMode ( 'own' ) }
@@ -234,12 +286,43 @@ export function ApiKeySettings({
234286 Use Your Own Key
235287 </ h3 >
236288 < p className = "text-sm text-gray-600 mt-1" >
237- Free with Google's generous quota
289+ Free with Google's quota
238290 </ p >
239291 < ul className = "text-xs text-gray-500 mt-2 space-y-1" >
240- < li > • No additional costs from us</ li >
241- < li > • Direct Google API billing</ li >
242- < li > • Full control over usage</ li >
292+ < li > • No costs from us</ li >
293+ < li > • Direct Google billing</ li >
294+ < li > • Full control</ li >
295+ </ ul >
296+ </ div >
297+ </ div >
298+ </ button >
299+
300+ { /* Access Code Option */ }
301+ < button
302+ onClick = { ( ) => setMode ( 'code' ) }
303+ className = { `p-4 border-2 rounded-lg text-left transition-all ${
304+ mode === 'code'
305+ ? 'border-purple-500 bg-purple-50'
306+ : 'border-gray-200 hover:border-gray-300'
307+ } `}
308+ >
309+ < div className = "flex items-start gap-3" >
310+ < div
311+ className = { `mt-1 ${ mode === 'code' ? 'text-purple-600' : 'text-gray-400' } ` }
312+ >
313+ < Ticket className = "w-5 h-5" />
314+ </ div >
315+ < div className = "flex-1" >
316+ < h3 className = "font-semibold text-gray-900" >
317+ Use Access Code
318+ </ h3 >
319+ < p className = "text-sm text-gray-600 mt-1" >
320+ Promotional or trial code
321+ </ p >
322+ < ul className = "text-xs text-gray-500 mt-2 space-y-1" >
323+ < li > • Free trial access</ li >
324+ < li > • Special promotions</ li >
325+ < li > • Beta access</ li >
243326 </ ul >
244327 </ div >
245328 </ div >
@@ -269,8 +352,8 @@ export function ApiKeySettings({
269352 </ p >
270353 < ul className = "text-xs text-gray-500 mt-2 space-y-1" >
271354 < li > • No API key needed</ li >
272- < li > • Simple monthly billing</ li >
273- < li > • Transparent cost tracking</ li >
355+ < li > • Monthly billing</ li >
356+ < li > • Cost tracking</ li >
274357 </ ul >
275358 </ div >
276359 </ div >
@@ -358,6 +441,84 @@ export function ApiKeySettings({
358441 </ div >
359442 ) }
360443
444+ { /* Access Code Configuration */ }
445+ { mode === 'code' && (
446+ < div className = "space-y-4 p-4 bg-purple-50 rounded-lg border border-purple-200" >
447+ < div >
448+ < label
449+ htmlFor = "accessCode"
450+ className = "block text-sm font-medium text-gray-700 mb-2"
451+ >
452+ Access Code
453+ </ label >
454+ < input
455+ id = "accessCode"
456+ type = "text"
457+ value = { accessCode }
458+ onChange = { e => {
459+ setAccessCode ( formatAccessCode ( e . target . value ) )
460+ setValidationStatus ( 'idle' )
461+ } }
462+ placeholder = "000-0000-000"
463+ maxLength = { 12 }
464+ className = "w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-transparent font-mono text-lg tracking-wider"
465+ />
466+ { validationStatus === 'valid' && (
467+ < div className = "flex items-center gap-2 mt-2 text-sm text-emerald-600" >
468+ < CheckCircle2 className = "w-4 h-4" />
469+ < span > { validationMessage } </ span >
470+ </ div >
471+ ) }
472+ { validationStatus === 'invalid' && (
473+ < div className = "flex items-center gap-2 mt-2 text-sm text-red-600" >
474+ < AlertCircle className = "w-4 h-4" />
475+ < span > { validationMessage } </ span >
476+ </ div >
477+ ) }
478+ </ div >
479+
480+ < div className = "space-y-2" >
481+ < h4 className = "text-sm font-medium text-gray-700" >
482+ About Access Codes:
483+ </ h4 >
484+ < ul className = "text-sm text-gray-600 space-y-2" >
485+ < li className = "flex items-start gap-2" >
486+ < span className = "text-purple-600" > •</ span >
487+ < span >
488+ Access codes are provided for free trials, promotional
489+ offers, or beta testing
490+ </ span >
491+ </ li >
492+ < li className = "flex items-start gap-2" >
493+ < span className = "text-purple-600" > •</ span >
494+ < span >
495+ Enter the code exactly as provided (format: 000-0000-000)
496+ </ span >
497+ </ li >
498+ < li className = "flex items-start gap-2" >
499+ < span className = "text-purple-600" > •</ span >
500+ < span > Codes may have expiration dates or usage limits</ span >
501+ </ li >
502+ < li className = "flex items-start gap-2" >
503+ < span className = "text-purple-600" > •</ span >
504+ < span >
505+ Contact support if you need an access code or have issues
506+ redeeming one
507+ </ span >
508+ </ li >
509+ </ ul >
510+ </ div >
511+
512+ < div className = "p-3 bg-purple-100 rounded-lg" >
513+ < p className = "text-xs text-purple-800" >
514+ < strong > Note:</ strong > Access codes use our API
515+ infrastructure, so you don't need your own Gemini API key.
516+ Perfect for trying out the service!
517+ </ p >
518+ </ div >
519+ </ div >
520+ ) }
521+
361522 { /* Paid Service Configuration */ }
362523 { mode === 'paid' && (
363524 < div className = "space-y-4 p-4 bg-emerald-50 rounded-lg border border-emerald-200" >
@@ -417,7 +578,9 @@ export function ApiKeySettings({
417578 ? 'Validating...'
418579 : mode === 'own'
419580 ? 'Validate & Save'
420- : 'Save Settings' }
581+ : mode === 'code'
582+ ? 'Redeem Code'
583+ : 'Save Settings' }
421584 </ Button >
422585 </ div >
423586 </ div >
@@ -452,6 +615,10 @@ export function getCurrentApiKey(): string | null {
452615 if ( config ?. mode === 'own' && config . ownKey ) {
453616 return config . ownKey
454617 }
618+ if ( config ?. mode === 'code' && config . accessCode ) {
619+ // In code mode, use the developer's API key from environment
620+ return import . meta. env . VITE_GEMINI_API_KEY || null
621+ }
455622 // Fall back to environment variable
456623 return import . meta. env . VITE_GEMINI_API_KEY || null
457624}
0 commit comments