import * as React from 'react' import { useCallback, useEffect, useState } from 'react' import type { CommandResultDisplay } from '../../commands.js' import { TEARDROP_ASTERISK } from '../../constants/figures.js' import { useExitOnCtrlCDWithKeybindings } from '../../hooks/useExitOnCtrlCDWithKeybindings.js' import { setClipboard } from '@anthropic/ink' // eslint-disable-next-line custom-rules/prefer-use-keybindings -- enter to copy link import { Box, Link, Text, useInput } from '@anthropic/ink' import { useKeybinding } from '../../keybindings/useKeybinding.js' import { logEvent } from '../../services/analytics/index.js' import { fetchReferralRedemptions, formatCreditAmount, getCachedOrFetchPassesEligibility, } from '../../services/api/referral.js' import type { ReferralRedemptionsResponse, ReferrerRewardInfo, } from '../../services/oauth/types.js' import { count } from '../../utils/array.js' import { logError } from '../../utils/log.js' import { Pane } from '@anthropic/ink' type PassStatus = { passNumber: number isAvailable: boolean } type Props = { onDone: ( result?: string, options?: { display?: CommandResultDisplay }, ) => void } export function Passes({ onDone }: Props): React.ReactNode { const [loading, setLoading] = useState(true) const [passStatuses, setPassStatuses] = useState([]) const [isAvailable, setIsAvailable] = useState(false) const [referralLink, setReferralLink] = useState(null) const [referrerReward, setReferrerReward] = useState< ReferrerRewardInfo | null | undefined >(undefined) const exitState = useExitOnCtrlCDWithKeybindings(() => onDone('Guest passes dialog dismissed', { display: 'system' }), ) const handleCancel = useCallback(() => { onDone('Guest passes dialog dismissed', { display: 'system' }) }, [onDone]) useKeybinding('confirm:no', handleCancel, { context: 'Confirmation' }) useInput((_input, key) => { if (key.return && referralLink) { void setClipboard(referralLink).then(raw => { if (raw) process.stdout.write(raw) logEvent('tengu_guest_passes_link_copied', {}) onDone(`Referral link copied to clipboard!`) }) } }) useEffect(() => { async function loadPassesData() { try { // Check eligibility first (uses cache if available) const eligibilityData = await getCachedOrFetchPassesEligibility() if (!eligibilityData || !eligibilityData.eligible) { setIsAvailable(false) setLoading(false) return } setIsAvailable(true) // Store the referral link if available if (eligibilityData.referral_code_details?.referral_link) { setReferralLink(eligibilityData.referral_code_details.referral_link) } // Store referrer reward info for v1 campaign messaging setReferrerReward(eligibilityData.referrer_reward) // Use the campaign returned from eligibility for redemptions const campaign = eligibilityData.referral_code_details?.campaign ?? 'claude_code_guest_pass' // Fetch redemptions data let redemptionsData: ReferralRedemptionsResponse try { redemptionsData = await fetchReferralRedemptions(campaign) } catch (err) { logError(err as Error) setIsAvailable(false) setLoading(false) return } // Build pass statuses array const redemptions = redemptionsData.redemptions || [] const maxRedemptions = redemptionsData.limit || 3 const statuses: PassStatus[] = [] for (let i = 0; i < maxRedemptions; i++) { const redemption = redemptions[i] statuses.push({ passNumber: i + 1, isAvailable: !redemption, }) } setPassStatuses(statuses) setLoading(false) } catch (err) { // For any error, just show passes as not available logError(err as Error) setIsAvailable(false) setLoading(false) } } void loadPassesData() }, []) if (loading) { return ( Loading guest pass information… {exitState.pending ? ( <>Press {exitState.keyName} again to exit ) : ( <>Esc to cancel )} ) } if (!isAvailable) { return ( Guest passes are not currently available. {exitState.pending ? ( <>Press {exitState.keyName} again to exit ) : ( <>Esc to cancel )} ) } const availableCount = count(passStatuses, p => p.isAvailable) // Sort passes: available first, then redeemed const sortedPasses = [...passStatuses].sort( (a, b) => +b.isAvailable - +a.isAvailable, ) // ASCII art for tickets const renderTicket = (pass: PassStatus) => { const isRedeemed = !pass.isAvailable if (isRedeemed) { // Grayed out redeemed ticket with slashes return ( {'┌─────────╱'} {` ) CC ${TEARDROP_ASTERISK} ┊╱`} {'└───────╱'} ) } return ( {'┌──────────┐'} {' ) CC '} {TEARDROP_ASTERISK} {' ┊ ( '} {'└──────────┘'} ) } return ( Guest passes · {availableCount} left {sortedPasses.slice(0, 3).map(pass => renderTicket(pass))} {referralLink && ( {referralLink} )} {referrerReward ? `Share a free week of Claude Code with friends. If they love it and subscribe, you'll get ${formatCreditAmount(referrerReward)} of extra usage to keep building. ` : 'Share a free week of Claude Code with friends. '} Terms apply. {exitState.pending ? ( <>Press {exitState.keyName} again to exit ) : ( <>Enter to copy link · Esc to cancel )} ) }