UNPKG

@teerai/teer-react

Version:

React components and hooks for Teer billing integration

417 lines (413 loc) 12.1 kB
// src/context/TeerContext.tsx import * as React from "react"; // src/api/teerApi.ts var TeerApi = class { constructor(publishableKey) { this.baseUrl = "https://api.teer.ai/v1"; this.publishableKey = publishableKey; } /** * Make a request to the Teer API */ async request(method, path, data, signal) { const url = `${this.baseUrl}${path}`; const headers = { "Content-Type": "application/json", Authorization: `Bearer ${this.publishableKey}` }; const options = { method, headers, credentials: "include", signal // Add abort signal to request }; if (data) { options.body = JSON.stringify(data); } try { const response = await fetch(url, options); if (signal?.aborted) { throw new DOMException("Request aborted", "AbortError"); } if (!response.ok) { const errorData = await response.json().catch(() => ({})); throw new Error(errorData.message || `API request failed with status ${response.status}`); } return response.json(); } catch (error) { if (error instanceof DOMException && error.name === "AbortError") { throw error; } if (error instanceof Error) { throw error; } throw new Error("Unknown error occurred during API request"); } } /** * Get billing configuration */ async getBillingConfig(signal) { return this.request("GET", "/billing/config", void 0, signal); } /** * Get customer by ID */ async getCustomer(customerId, signal) { return this.request("GET", `/customers/${customerId}`, void 0, signal); } /** * Get customer subscriptions */ async getSubscriptions(customerId, signal) { return this.request("GET", `/customers/${customerId}/subscriptions`, void 0, signal); } /** * Create a checkout session */ async createCheckoutSession(options, signal) { return this.request("POST", "/checkout/sessions", options, signal); } /** * Create a billing portal session */ async createBillingPortalSession(options, signal) { return this.request("POST", "/billing/portal/sessions", options, signal); } /** * Report usage */ async reportUsage(subscriptionItemId, quantity, signal) { return this.request( "POST", `/subscription-items/${subscriptionItemId}/usage`, { quantity, timestamp: Math.floor(Date.now() / 1e3) }, signal ); } }; // src/context/TeerContext.tsx var { createContext, useEffect, useMemo, useState, useRef, useCallback } = React; var TeerContext = createContext(void 0); var TeerProvider = ({ publishableKey, customerId, customerEmail, successUrl, cancelUrl, fetchOnMount = true, children }) => { const api = useMemo(() => new TeerApi(publishableKey), [publishableKey]); const [isLoading, setIsLoading] = useState(false); const [isReady, setIsReady] = useState(false); const [error, setError] = useState(null); const [billingConfig, setBillingConfig] = useState(null); const [customer, setCustomer] = useState(null); const [subscriptions, setSubscriptions] = useState([]); const isFetchingRef = useRef(false); const abortControllerRef = useRef(null); const publishableKeyRef = useRef(publishableKey); const customerIdRef = useRef(customerId); useEffect(() => { publishableKeyRef.current = publishableKey; customerIdRef.current = customerId; }, [publishableKey, customerId]); const fetchBillingConfig = useCallback(async () => { if (isFetchingRef.current) { return; } try { if (abortControllerRef.current) { abortControllerRef.current.abort(); } abortControllerRef.current = new AbortController(); isFetchingRef.current = true; setIsLoading(true); setError(null); const signal = abortControllerRef.current.signal; const config = await api.getBillingConfig(signal); if (signal.aborted) return; setBillingConfig(config); if (customerIdRef.current) { const customerData = await api.getCustomer(customerIdRef.current, signal); if (signal.aborted) return; setCustomer(customerData); const subscriptionsData = await api.getSubscriptions(customerIdRef.current, signal); if (signal.aborted) return; setSubscriptions(subscriptionsData); } else { setCustomer(null); setSubscriptions([]); } setIsReady(true); } catch (err) { if (err instanceof DOMException && err.name === "AbortError") { return; } setError(err instanceof Error ? err : new Error("Failed to fetch billing configuration")); } finally { isFetchingRef.current = false; setIsLoading(false); } }, [api]); const refetch = useCallback(async () => { await fetchBillingConfig(); }, [fetchBillingConfig]); const checkout = useCallback( async (options) => { let localLoading = false; const abortController = new AbortController(); const signal = abortController.signal; try { if (!isFetchingRef.current) { setIsLoading(true); localLoading = true; } const checkoutOptions = typeof options === "string" ? { priceId: options } : options; if (!checkoutOptions.successUrl && successUrl) { checkoutOptions.successUrl = successUrl; } if (!checkoutOptions.cancelUrl && cancelUrl) { checkoutOptions.cancelUrl = cancelUrl; } if (!checkoutOptions.customerId && customerIdRef.current) { checkoutOptions.customerId = customerIdRef.current; } if (!checkoutOptions.customerEmail && customerEmail) { checkoutOptions.customerEmail = customerEmail; } const session = await api.createCheckoutSession(checkoutOptions, signal); if (signal.aborted) { throw new DOMException("Request aborted", "AbortError"); } if (typeof window !== "undefined" && session.url) { window.location.href = session.url; } return session; } catch (err) { if (err instanceof DOMException && err.name === "AbortError") { throw err; } const error2 = err instanceof Error ? err : new Error("Failed to create checkout session"); setError(error2); throw error2; } finally { abortController.abort(); if (localLoading) { setIsLoading(false); } } }, [api, successUrl, cancelUrl, customerEmail] ); const billingPortal = useCallback( async (options) => { let localLoading = false; const abortController = new AbortController(); const signal = abortController.signal; try { if (!isFetchingRef.current) { setIsLoading(true); localLoading = true; } const portalOptions = options || {}; if (!portalOptions.customerId && customerIdRef.current) { portalOptions.customerId = customerIdRef.current; } if (!portalOptions.returnUrl && typeof window !== "undefined") { portalOptions.returnUrl = window.location.href; } const session = await api.createBillingPortalSession(portalOptions, signal); if (signal.aborted) { throw new DOMException("Request aborted", "AbortError"); } if (typeof window !== "undefined" && session.url) { window.location.href = session.url; } return session; } catch (err) { if (err instanceof DOMException && err.name === "AbortError") { throw err; } const error2 = err instanceof Error ? err : new Error("Failed to create billing portal session"); setError(error2); throw error2; } finally { abortController.abort(); if (localLoading) { setIsLoading(false); } } }, [api] ); const reportUsage = useCallback( async (subscriptionItemId, quantity) => { let localLoading = false; const abortController = new AbortController(); const signal = abortController.signal; try { if (!isFetchingRef.current) { setIsLoading(true); localLoading = true; } const result = await api.reportUsage(subscriptionItemId, quantity, signal); if (signal.aborted) { throw new DOMException("Request aborted", "AbortError"); } return result; } catch (err) { if (err instanceof DOMException && err.name === "AbortError") { throw err; } const error2 = err instanceof Error ? err : new Error("Failed to report usage"); setError(error2); throw error2; } finally { abortController.abort(); if (localLoading) { setIsLoading(false); } } }, [api] ); useEffect(() => { return () => { if (abortControllerRef.current) { abortControllerRef.current.abort(); } }; }, []); useEffect(() => { if (fetchOnMount) { fetchBillingConfig(); } return () => { if (abortControllerRef.current) { abortControllerRef.current.abort(); } }; }, [fetchOnMount, fetchBillingConfig]); const contextValue = useMemo( () => ({ isLoading, isReady, error, billingConfig, customer, subscriptions, refetch, checkout, billingPortal, reportUsage }), [isLoading, isReady, error, billingConfig, customer, subscriptions, refetch, checkout, billingPortal, reportUsage] ); return /* @__PURE__ */ React.createElement(TeerContext.Provider, { value: contextValue }, children); }; // src/hooks/useTeer.ts import { useContext } from "react"; function useTeer() { const context = useContext(TeerContext); if (context === void 0) { throw new Error("useTeer must be used within a TeerProvider"); } return context; } function useBillingConfig() { const { billingConfig, isLoading, error } = useTeer(); return { billingConfig, isLoading, error }; } function useCustomer() { const { customer, isLoading, error } = useTeer(); return { customer, isLoading, error }; } function useSubscriptions() { const { subscriptions, isLoading, error } = useTeer(); return { subscriptions, isLoading, error }; } function useCheckout() { const { checkout, isLoading, error } = useTeer(); return { checkout, isLoading, error }; } function useBillingPortal() { const { billingPortal, isLoading, error } = useTeer(); return { billingPortal, isLoading, error }; } function useUsageReporting() { const { reportUsage, isLoading, error } = useTeer(); return { reportUsage, isLoading, error }; } // src/utils/index.ts function getActiveSubscriptions(subscriptions) { return subscriptions.filter((subscription) => subscription.status === "active" || subscription.status === "trialing"); } function getPlanById(plans, planId) { return plans.find((plan) => plan.id === planId); } function getPriceById(plans, priceId) { for (const plan of plans) { const price = plan.prices.find((price2) => price2.id === priceId); if (price) { return price; } } return void 0; } function getActivePlanPrice(plan, options = {}) { const { currency = "usd", interval = "month" } = options; let price = plan.prices.find((price2) => price2.currency.toLowerCase() === currency.toLowerCase() && price2.interval === interval); if (!price) { price = plan.prices.find((price2) => price2.currency.toLowerCase() === currency.toLowerCase()); } if (!price && plan.prices.length > 0) { price = plan.prices[0]; } return price; } export { TeerContext, TeerProvider, getActivePlanPrice, getActiveSubscriptions, getPlanById, getPriceById, useBillingConfig, useBillingPortal, useCheckout, useCustomer, useSubscriptions, useTeer, useUsageReporting }; //# sourceMappingURL=index.js.map