import { db } from '../config/firebase' import { RevenueCatEvent } from '../types' import { getEnvValue } from '../config/env' import * as admin from 'firebase-admin' // Product IDs that grant premium access (comma-separated, configurable via env) const DEFAULT_PREMIUM_PRODUCT_IDS = 'closer_premium_monthly,closer_premium_yearly' const PRODUCT_ID_ALLOWLIST = new Set( getEnvValue('REVENUECAT_PREMIUM_PRODUCT_IDS') .split(',') .map(id => id.trim()) .filter(id => id.length > 0) ) if (PRODUCT_ID_ALLOWLIST.size === 0) { console.warn('[entitlement] No product IDs in allowlist - entitlement validation will be bypassed') } const PREMIUM_ACTIVE_TYPES = new Set([ 'INITIAL_PURCHASE', 'RENEWAL', 'PRODUCT_CHANGE', 'TRANSFER', 'UNCANCELLATION', ]) const PREMIUM_REVOKED_TYPES = new Set([ 'EXPIRATION', 'CANCELLATION', 'SUBSCRIBER_ALIAS', ]) const PREMIUM_ENTITLEMENT_ID = 'closer_premium' /** * Checks if premium is currently active for a user. * Verifies both hasPremium flag AND expiration timestamp. */ export async function verifyPremiumActive(uid: string): Promise { const userDoc = await db().collection('users').doc(uid).get() const data = userDoc.data() if (!data) return false // hasPremium must be true AND expiration must be in the future if (!data.hasPremium) return false const expiresAt = data.premiumExpiresAt as FirebaseFirestore.Timestamp | Date | undefined if (!expiresAt) return false const now = new Date() const expirationDate = typeof (expiresAt as any).toDate === 'function' ? (expiresAt as FirebaseFirestore.Timestamp).toDate() : (expiresAt as Date) return expirationDate > now } export async function syncEntitlement(event: RevenueCatEvent): Promise { const { type, app_user_id: uid, id: eventId, product_id } = event.event // Idempotency: atomically create the event record. If it already exists, // another concurrent request processed it and we abort cleanly. const eventRef = db().collection('entitlement_events').doc(eventId) try { await eventRef.create({ processedAt: new Date() }) } catch (err: any) { if (err?.code === 6 || err?.message?.includes('ALREADY_EXISTS')) { console.log(`[entitlement] skipping duplicate event: ${eventId}`) return } throw err } // Validate product_id against allowlist if (!PRODUCT_ID_ALLOWLIST.has(product_id)) { console.log(`[entitlement] ignored event for unknown product_id: ${product_id}`) return } // Verify entitlement_id matches expected premium entitlement const entitlementId = event.event.entitlement?.identifier || event.event.entitlement_id if (entitlementId !== PREMIUM_ENTITLEMENT_ID) { console.log(`[entitlement] ignored event for wrong entitlement: ${entitlementId} (expected: ${PREMIUM_ENTITLEMENT_ID})`) return } const userRef = db().collection('users').doc(uid) if (PREMIUM_ACTIVE_TYPES.has(type)) { const updates: { hasPremium: true; premiumExpiresAt?: FirebaseFirestore.Timestamp } = { hasPremium: true } // Store expiration timestamp if provided const expirationAtMs = event.event.expiration_at_ms if (expirationAtMs !== undefined) { updates.premiumExpiresAt = admin.firestore.Timestamp.fromMillis(expirationAtMs) } await userRef.update(updates) console.log(`[entitlement] hasPremium=true for ${uid} (${type})`) return } if (PREMIUM_REVOKED_TYPES.has(type)) { const updates: { hasPremium: false; premiumExpiresAt: null } = { hasPremium: false, premiumExpiresAt: null } await userRef.update(updates) console.log(`[entitlement] hasPremium=false for ${uid} (${type})`) return } console.log(`[entitlement] ignored event type: ${type}`) }