UNPKG

cosmic-payments

Version:

A payments library for cosmic.new. Designed to be used and deployed on cosmic.new

280 lines (279 loc) 10.7 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.useCosmicPayments = useCosmicPayments; const react_1 = require("react"); /** * Global singleton API client that maintains state across all hook instances. */ class CosmicPaymentsClient { constructor() { // eslint-disable-next-line @typescript-eslint/no-explicit-any this.activeRequests = new Map(); this.subscribers = new Set(); this.currentState = { loading: false, error: null }; this.productCache = null; this.cacheTimestamp = 0; this.CACHE_DURATION = 5 * 60 * 1000; // 5 minutes // Add caching for access checks this.accessCache = new Map(); this.purchaseHistoryCache = null; this.subscriptionsCache = null; this.ACCESS_CACHE_DURATION = 60 * 1000; // 1 minute for access checks /** * Get all products with optional filtering */ this.getProducts = async (filter = 'all') => { // Check cache first const now = Date.now(); if (this.productCache && (now - this.cacheTimestamp) < this.CACHE_DURATION) { return this.filterProducts(this.productCache, filter); } // Fetch both types of products in parallel const [subResult, nonSubResult] = await Promise.all([ this.makeApiCall('get-all-subscription-products'), this.makeApiCall('get-all-non-subscription-products') ]); if (!subResult || !nonSubResult) return null; // Combine and mark products const allProducts = [ ...(subResult.products || []).map(p => ({ ...p, is_subscription: true })), ...(nonSubResult.products || []).map(p => ({ ...p, is_subscription: false })) ]; // Update cache this.productCache = allProducts; this.cacheTimestamp = now; return this.filterProducts(allProducts, filter); }; /** * Check if user has access to a product */ this.hasAccess = async (productId) => { // Check cache first const cached = this.accessCache.get(productId); const now = Date.now(); if (cached && (now - cached.timestamp) < this.ACCESS_CACHE_DURATION) { return cached.result; } const [history, subscriptions] = await Promise.all([ this.getPurchaseHistory(), this.getActiveSubscriptions() ]); // Check one-time purchases const hasPurchased = history?.some(item => item.product_id === productId) || false; // Check active subscriptions const hasActiveSubscription = subscriptions?.some(sub => sub.subscription_product_id === productId && sub.subscription_status === 'active') || false; const result = hasPurchased || hasActiveSubscription; // Cache the result this.accessCache.set(productId, { result, timestamp: now }); return result; }; /** * Get purchase history for authenticated users */ this.getPurchaseHistory = async () => { // Check cache first const now = Date.now(); if (this.purchaseHistoryCache && (now - this.purchaseHistoryCache.timestamp) < this.ACCESS_CACHE_DURATION) { return this.purchaseHistoryCache.data; } const result = await this.makeApiCall('get-purchase-history'); const data = result?.purchaseHistory || null; // Cache the result this.purchaseHistoryCache = { data, timestamp: now }; return data; }; /** * Get active subscriptions for authenticated users */ this.getActiveSubscriptions = async () => { // Check cache first const now = Date.now(); if (this.subscriptionsCache && (now - this.subscriptionsCache.timestamp) < this.ACCESS_CACHE_DURATION) { return this.subscriptionsCache.data; } const result = await this.makeApiCall('get-active-subscriptions'); const data = result?.activeSubscriptions || null; // Cache the result this.subscriptionsCache = { data, timestamp: now }; return data; }; /** * Create checkout and redirect immediately */ this.checkout = async (params) => { const result = await this.makeApiCall('create-checkout-link', params); if (result?.url) { // Clear caches since user might be making a purchase this.clearCaches(); window.location.href = result.url; } }; /** * Open billing portal for subscription management */ this.openBillingPortal = async () => { const result = await this.makeApiCall('get-billing-portal'); if (result?.url) { // Clear caches since user might be managing subscriptions this.clearCaches(); window.location.href = result.url; } }; } /** * Notify all subscribers of state changes */ notifySubscribers() { this.subscribers.forEach(callback => callback(this.currentState)); } /** * Update loading state based on active requests */ updateLoading() { const newLoading = this.activeRequests.size > 0; if (newLoading !== this.currentState.loading) { this.currentState.loading = newLoading; this.notifySubscribers(); } } /** * Set error state and notify subscribers */ setError(error) { if (error !== this.currentState.error) { this.currentState.error = error; this.notifySubscribers(); } } /** * Subscribe to state changes */ subscribe(callback) { this.subscribers.add(callback); callback(this.currentState); return () => { this.subscribers.delete(callback); }; } /** * Clear all caches (useful after purchases or subscription changes) */ clearCaches() { this.accessCache.clear(); this.purchaseHistoryCache = null; this.subscriptionsCache = null; this.productCache = null; } /** * Make an API call with request deduplication and state management */ // eslint-disable-next-line @typescript-eslint/no-explicit-any async makeApiCall(actionType, additionalData) { const requestKey = `${actionType}-${JSON.stringify(additionalData || {})}`; // Check if there's already an active request for this exact call const existingRequest = this.activeRequests.get(requestKey); if (existingRequest) { // Return the existing promise instead of null return existingRequest; } // Create the promise for this request const requestPromise = (async () => { this.updateLoading(); this.setError(null); try { const response = await fetch('/api/cosmic-payments', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ actionType, ...additionalData, }), credentials: 'include', }); if (!response.ok) { let errorMessage = `HTTP error! Status: ${response.status}`; try { const errorData = await response.json(); errorMessage = errorData.error || errorMessage; } catch { // If we can't parse the error response, use the default message } throw new Error(errorMessage); } const data = await response.json(); if (data.error) { throw new Error(data.error); } return data; } catch (err) { const errorMessage = err instanceof Error ? err.message : 'An unknown error occurred'; console.error(`[useCosmicPayments] Error with ${actionType}:`, err); this.setError(errorMessage); return null; } finally { // Clean up the request from the map this.activeRequests.delete(requestKey); this.updateLoading(); } })(); // Store the promise this.activeRequests.set(requestKey, requestPromise); return requestPromise; } filterProducts(products, filter) { switch (filter) { case 'subscription': return products.filter(p => p.is_subscription); case 'one-time': return products.filter(p => !p.is_subscription); default: return products; } } } // Global singleton instance const cosmicPaymentsClient = new CosmicPaymentsClient(); /** * Simple hook for Cosmic Payments integration * * @example * ```tsx * const { getProducts, checkout, hasAccess, loading, error } = useCosmicPayments(); * * // Get products * const products = await getProducts('subscription'); // or 'one-time' or 'all' * * // Start checkout (with optional guest checkout) * await checkout({ * priceId: price.price_id, * productId: product.product_id, * allowGuestCheckout: true // Optional, defaults to false * }); * * // Check access * const hasAccess = await hasAccess('prod_123'); * ``` */ function useCosmicPayments() { const [state, setState] = (0, react_1.useState)({ loading: false, error: null }); (0, react_1.useEffect)(() => { const unsubscribe = cosmicPaymentsClient.subscribe(setState); return unsubscribe; }, []); return { getProducts: cosmicPaymentsClient.getProducts, checkout: cosmicPaymentsClient.checkout, openBillingPortal: cosmicPaymentsClient.openBillingPortal, hasAccess: cosmicPaymentsClient.hasAccess, getPurchaseHistory: cosmicPaymentsClient.getPurchaseHistory, getActiveSubscriptions: cosmicPaymentsClient.getActiveSubscriptions, loading: state.loading, error: state.error, }; }