UNPKG

@teerai/teer-react

Version:

React components and hooks for Teer billing integration

450 lines (444 loc) 13.9 kB
"use strict"; var __create = Object.create; var __defProp = Object.defineProperty; var __getOwnPropDesc = Object.getOwnPropertyDescriptor; var __getOwnPropNames = Object.getOwnPropertyNames; var __getProtoOf = Object.getPrototypeOf; var __hasOwnProp = Object.prototype.hasOwnProperty; var __export = (target, all) => { for (var name in all) __defProp(target, name, { get: all[name], enumerable: true }); }; var __copyProps = (to, from, except, desc) => { if (from && typeof from === "object" || typeof from === "function") { for (let key of __getOwnPropNames(from)) if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); } return to; }; var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps( // If the importer is in node compatibility mode or this is not an ESM // file that has been converted to a CommonJS file using a Babel- // compatible transform (i.e. "__esModule" has not been set), then set // "default" to the CommonJS "module.exports" for node compatibility. isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target, mod )); var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod); // src/index.ts var index_exports = {}; __export(index_exports, { TeerContext: () => TeerContext, TeerProvider: () => TeerProvider, getActivePlanPrice: () => getActivePlanPrice, getActiveSubscriptions: () => getActiveSubscriptions, getPlanById: () => getPlanById, getPriceById: () => getPriceById, useBillingConfig: () => useBillingConfig, useBillingPortal: () => useBillingPortal, useCheckout: () => useCheckout, useCustomer: () => useCustomer, useSubscriptions: () => useSubscriptions, useTeer: () => useTeer, useUsageReporting: () => useUsageReporting }); module.exports = __toCommonJS(index_exports); // src/context/TeerContext.tsx var React = __toESM(require("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 var import_react = require("react"); function useTeer() { const context = (0, import_react.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; } //# sourceMappingURL=index.cjs.map