@teerai/teer-react
Version:
React components and hooks for Teer billing integration
417 lines (413 loc) • 12.1 kB
JavaScript
// 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