Created
January 29, 2022 16:46
-
-
Save christiangenco/63409671e0e91d11b093b62311673f95 to your computer and use it in GitHub Desktop.
useStripe React Hooks for Stripe Firebase extension
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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> | |
); | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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