import axios from 'axios' import { getOauthConfig } from '../../constants/oauth.js' import { getOauthAccountInfo, getSubscriptionType, isClaudeAISubscriber, } from '../../utils/auth.js' import { getGlobalConfig, saveGlobalConfig } from '../../utils/config.js' import { logForDebugging } from '../../utils/debug.js' import { logError } from '../../utils/log.js' import { isEssentialTrafficOnly } from '../../utils/privacyLevel.js' import { getOAuthHeaders, prepareApiRequest } from '../../utils/teleport/api.js' import type { ReferralCampaign, ReferralEligibilityResponse, ReferralRedemptionsResponse, ReferrerRewardInfo, } from '../oauth/types.js' // Cache expiration time: 24 hours (eligibility changes only on subscription/experiment changes) const CACHE_EXPIRATION_MS = 24 * 60 * 60 * 1000 // Track in-flight fetch to prevent duplicate API calls let fetchInProgress: Promise | null = null export async function fetchReferralEligibility( campaign: ReferralCampaign = 'claude_code_guest_pass', ): Promise { const { accessToken, orgUUID } = await prepareApiRequest() const headers = { ...getOAuthHeaders(accessToken), 'x-organization-uuid': orgUUID, } const url = `${getOauthConfig().BASE_API_URL}/api/oauth/organizations/${orgUUID}/referral/eligibility` const response = await axios.get(url, { headers, params: { campaign }, timeout: 5000, // 5 second timeout for background fetch }) return response.data } export async function fetchReferralRedemptions( campaign: string = 'claude_code_guest_pass', ): Promise { const { accessToken, orgUUID } = await prepareApiRequest() const headers = { ...getOAuthHeaders(accessToken), 'x-organization-uuid': orgUUID, } const url = `${getOauthConfig().BASE_API_URL}/api/oauth/organizations/${orgUUID}/referral/redemptions` const response = await axios.get(url, { headers, params: { campaign }, timeout: 10000, // 10 second timeout }) return response.data } /** * Prechecks for if user can access guest passes feature */ function shouldCheckForPasses(): boolean { return !!( getOauthAccountInfo()?.organizationUuid && isClaudeAISubscriber() && getSubscriptionType() === 'max' ) } /** * Check cached passes eligibility from GlobalConfig * Returns current cached state and cache status */ export function checkCachedPassesEligibility(): { eligible: boolean needsRefresh: boolean hasCache: boolean } { if (!shouldCheckForPasses()) { return { eligible: false, needsRefresh: false, hasCache: false, } } const orgId = getOauthAccountInfo()?.organizationUuid if (!orgId) { return { eligible: false, needsRefresh: false, hasCache: false, } } const config = getGlobalConfig() const cachedEntry = config.passesEligibilityCache?.[orgId] if (!cachedEntry) { // No cached entry, needs fetch return { eligible: false, needsRefresh: true, hasCache: false, } } const { eligible, timestamp } = cachedEntry const now = Date.now() const needsRefresh = now - timestamp > CACHE_EXPIRATION_MS return { eligible, needsRefresh, hasCache: true, } } const CURRENCY_SYMBOLS: Record = { USD: '$', EUR: '€', GBP: '£', BRL: 'R$', CAD: 'CA$', AUD: 'A$', NZD: 'NZ$', SGD: 'S$', } export function formatCreditAmount(reward: ReferrerRewardInfo): string { const symbol = CURRENCY_SYMBOLS[reward.currency] ?? `${reward.currency} ` const amount = reward.amount_minor_units / 100 const formatted = amount % 1 === 0 ? amount.toString() : amount.toFixed(2) return `${symbol}${formatted}` } /** * Get cached referrer reward info from eligibility cache * Returns the reward info if the user is in a v1 campaign, null otherwise */ export function getCachedReferrerReward(): ReferrerRewardInfo | null { const orgId = getOauthAccountInfo()?.organizationUuid if (!orgId) return null const config = getGlobalConfig() const cachedEntry = config.passesEligibilityCache?.[orgId] return cachedEntry?.referrer_reward ?? null } /** * Get the cached remaining passes count from eligibility cache * Returns the number of remaining passes, or null if not available */ export function getCachedRemainingPasses(): number | null { const orgId = getOauthAccountInfo()?.organizationUuid if (!orgId) return null const config = getGlobalConfig() const cachedEntry = config.passesEligibilityCache?.[orgId] return cachedEntry?.remaining_passes ?? null } /** * Fetch passes eligibility and store in GlobalConfig * Returns the fetched response or null on error */ export async function fetchAndStorePassesEligibility(): Promise { // Return existing promise if fetch is already in progress if (fetchInProgress) { logForDebugging('Passes: Reusing in-flight eligibility fetch') return fetchInProgress } const orgId = getOauthAccountInfo()?.organizationUuid if (!orgId) { return null } // Store the promise to share with concurrent calls fetchInProgress = (async () => { try { const response = await fetchReferralEligibility() const cacheEntry = { ...response, timestamp: Date.now(), } saveGlobalConfig(current => ({ ...current, passesEligibilityCache: { ...current.passesEligibilityCache, [orgId]: cacheEntry, }, })) logForDebugging( `Passes eligibility cached for org ${orgId}: ${response.eligible}`, ) return response } catch (error) { logForDebugging('Failed to fetch and cache passes eligibility') logError(error as Error) return null } finally { // Clear the promise when done fetchInProgress = null } })() return fetchInProgress } /** * Get cached passes eligibility data or fetch if needed * Main entry point for all eligibility checks * * This function never blocks on network - it returns cached data immediately * and fetches in the background if needed. On cold start (no cache), it returns * null and the passes command won't be available until the next session. */ export async function getCachedOrFetchPassesEligibility(): Promise { if (!shouldCheckForPasses()) { return null } const orgId = getOauthAccountInfo()?.organizationUuid if (!orgId) { return null } const config = getGlobalConfig() const cachedEntry = config.passesEligibilityCache?.[orgId] const now = Date.now() // No cache - trigger background fetch and return null (non-blocking) // The passes command won't be available this session, but will be next time if (!cachedEntry) { logForDebugging( 'Passes: No cache, fetching eligibility in background (command unavailable this session)', ) void fetchAndStorePassesEligibility() return null } // Cache exists but is stale - return stale cache and trigger background refresh if (now - cachedEntry.timestamp > CACHE_EXPIRATION_MS) { logForDebugging( 'Passes: Cache stale, returning cached data and refreshing in background', ) void fetchAndStorePassesEligibility() // Background refresh const { timestamp, ...response } = cachedEntry return response as ReferralEligibilityResponse } // Cache is fresh - return it immediately logForDebugging('Passes: Using fresh cached eligibility data') const { timestamp, ...response } = cachedEntry return response as ReferralEligibilityResponse } /** * Prefetch passes eligibility on startup */ export async function prefetchPassesEligibility(): Promise { // Skip network requests if nonessential traffic is disabled if (isEssentialTrafficOnly()) { return } void getCachedOrFetchPassesEligibility() }