Skip to content

Instantly share code, notes, and snippets.

@christiangenco
Created January 29, 2022 16:46
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save christiangenco/63409671e0e91d11b093b62311673f95 to your computer and use it in GitHub Desktop.
Save christiangenco/63409671e0e91d11b093b62311673f95 to your computer and use it in GitHub Desktop.
useStripe React Hooks for Stripe Firebase extension
import { Fragment, useEffect } from "react";
import {
useProducts,
useSubscriptions,
useStripeRole,
visitPortal,
useCreateCheckoutSession,
} from "hooks/useStripe";
// UI components
import PricingTiers from "components/PricingTiers";
import { LoadingIcon } from "utils/icons";
export default function Example() {
const {
loading: checkoutSessionLoading,
error: checkoutSessionError,
createCheckoutSession,
} = useCreateCheckoutSession();
const stripeRole = useStripeRole();
const subscriptions = useSubscriptions();
const {
products,
error: productsError,
loading: productsLoading,
} = useProducts();
// map the products to a new tiers object that works better for my PricingTiers component
const tiers = Object.values(products || {}).map((tier) => {
return {
...tier,
features: [],
mostPopular: true,
cta: `Buy ${tier.name}`,
};
});
// automatically redirect to Stripe's billing portal if the user has a subscription
useEffect(() => {
if (stripeRole) visitPortal({ returnUrl: window.location.origin });
}, [stripeRole]);
return (
<div>
{!stripeRole && (
<Fragment>
{!products && "Loading plans..."}
{products && (
<PricingTiers
loading={checkoutSessionLoading}
tiers={tiers}
onBuy={(priceId) => {
createCheckoutSession({
priceId,
success_url: window.location.origin,
});
}}
/>
)}
</Fragment>
)}
{stripeRole && (
<div className="sm:flex sm:flex-col sm:align-center">
<h1 className="text-5xl font-extrabold text-gray-900 sm:text-center">
<LoadingIcon className="h-12 w-12 text-blue-500 mr-5" />
Loading portal...
</h1>
</div>
)}
</div>
);
}
import { useState, useEffect } from "react";
// Stripe's official library doesn't work so let's remake it for React!
// https://stripe-subs-ext.web.app/
// import {
// getStripePayments,
// getProducts,
// } from "@stripe/firestore-stripe-payments";
import { getApp } from "firebase/app";
import {
getFirestore,
collection,
query,
where,
limit,
getDocs,
onSnapshot,
addDoc,
} from "firebase/firestore";
import { getFunctions, httpsCallable } from "firebase/functions";
import { getAuth, onAuthStateChanged } from "firebase/auth";
function useUser() {
const auth = getAuth();
const [user, setUser] = useState(null);
useEffect(() => {
return onAuthStateChanged(auth, setUser);
}, []);
return user;
}
export function useStripeRole() {
const user = useUser();
const [stripeRole, setStripeRole] = useState(null);
useEffect(() => {
if (user) {
user.getIdToken(true).then(() => {
user.getIdTokenResult().then((decodedToken) => {
setStripeRole(decodedToken?.claims?.stripeRole);
});
});
}
}, [user]);
return stripeRole;
}
export async function visitPortal({
zone = "us-central1",
returnUrl = window.location.href,
} = {}) {
const firebaseApp = getApp();
const createPortalLink = httpsCallable(
getFunctions(firebaseApp, zone),
"ext-firestore-stripe-payments-createPortalLink"
);
const { data } = await createPortalLink({
returnUrl,
// locale: "auto",
// https://stripe.com/docs/api/customer_portal/configuration
// configuration: "bpc_1JSEAKHYgolSBA358VNoc2Hs",
});
window.location.assign(data.url);
}
export function useCreateCheckoutSession({
productsCollection = "products",
customersCollection = "customers",
} = {}) {
const db = getFirestore();
const user = useUser();
const [loading, setLoading] = useState(false);
const [error, setError] = useState(false);
const createCheckoutSession = async ({
priceId,
// lineItems,
success_url = window.location.href,
cancel_url = window.location.href,
}) => {
if (!user?.uid) {
setError("You need to log in before signing up for a plan.");
return;
}
setLoading(true);
const checkoutSessionsRef = collection(
db,
customersCollection,
user.uid,
"checkout_sessions"
);
const docRef = await addDoc(checkoutSessionsRef, {
price: priceId,
// line_items: lineItems,
success_url,
cancel_url,
});
// wait for the CheckoutSession to get attached by the extension
onSnapshot(docRef, (snap) => {
const { error, url } = snap.data();
if (error) {
setLoading(false);
setError(error.message);
}
if (url) {
// setLoading(false);
window.location.assign(url);
}
});
};
return {
createCheckoutSession,
loading,
error,
};
}
export function useSubscriptions({
active = true,
customersCollection = "customers",
} = {}) {
const db = getFirestore();
const user = useUser();
const [subscriptions, setSubscriptions] = useState({});
useEffect(() => {
console.log("running useSubscriptions effect", user?.uid);
if (!user?.uid) return;
const subscriptionsRef = collection(
db,
customersCollection,
user.uid,
"subscriptions"
);
const queryArgs = [subscriptionsRef];
if (active) queryArgs.push(where("status", "in", ["trailing", "active"]));
getDocs(query(...queryArgs)).then((subscriptionsSnap) => {
setSubscriptions(
subscriptionsSnap.docs.map((subscriptionSnap) => {
return { id: subscriptionSnap.id, ...subscriptionSnap.data() };
})
);
});
}, [user]);
return subscriptions;
}
export function useProducts({
activeOnly = true,
includePrices = true,
productsCollection = "products",
} = {}) {
// I should be able to use getStripePayments from Firebase but I can't because it's broken:
// https://github.com/stripe/stripe-firebase-extensions/issues/327
// const firebaseApp = getApp();
// const stripePayments = getStripePayments(firebaseApp, {
// productsCollection: "products",
// customersCollection: "customers",
// });
// const [products, setProducts] = useState({});
// useEffect(() => {
// async function fetchProducts() {
// if (firebase) {
// const products = await getProducts(stripePayments, {
// includePrices: true,
// activeOnly: true,
// });
// setProducts(products);
// }
// }
// fetchProducts();
// }, [firebase, stripePayments]);
// return products;
const db = getFirestore();
const [products, setProducts] = useState({});
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
const queryArgs = [collection(db, productsCollection)];
if (activeOnly) queryArgs.push(where("active", "==", true));
getDocs(query(...queryArgs)).then((snap) => {
snap.forEach(async (doc) => {
const product = { id: doc.id, ...doc.data() };
if (includePrices) {
const pricesSnap = await getDocs(collection(doc.ref, "prices"));
product.prices = pricesSnap.docs.map((doc) => {
return { id: doc.id, ...doc.data() };
});
}
setProducts((oldProducts) => ({
...oldProducts,
[product.id]: product,
}));
});
});
}, []);
return { products, loading, error };
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment