From 56c90d9c831e8859443ccb5783c515c45ad3e5f1 Mon Sep 17 00:00:00 2001 From: Abi Raja Date: Thu, 4 Jan 2024 16:57:10 -0800 Subject: [PATCH] add direct link to Stripe Customer Portal --- .../src/components/hosted/AvatarDropdown.tsx | 8 +--- .../hosted/StripeCustomerPortalLink.tsx | 48 +++++++++++++++++++ frontend/src/components/hosted/types.ts | 4 ++ .../hosted/useAuthenticatedFetch.ts | 3 ++ 4 files changed, 57 insertions(+), 6 deletions(-) create mode 100644 frontend/src/components/hosted/StripeCustomerPortalLink.tsx diff --git a/frontend/src/components/hosted/AvatarDropdown.tsx b/frontend/src/components/hosted/AvatarDropdown.tsx index 2ea8f11..d587ef1 100644 --- a/frontend/src/components/hosted/AvatarDropdown.tsx +++ b/frontend/src/components/hosted/AvatarDropdown.tsx @@ -10,6 +10,7 @@ import { } from "../ui/dropdown-menu"; import { useStore } from "../../store/store"; import { capitalize } from "./utils"; +import StripeCustomerPortalLink from "./StripeCustomerPortalLink"; export default function AvatarDropdown() { const { user, isLoaded, isSignedIn } = useUser(); @@ -55,12 +56,7 @@ export default function AvatarDropdown() { - - Cancel subscription - + )} diff --git a/frontend/src/components/hosted/StripeCustomerPortalLink.tsx b/frontend/src/components/hosted/StripeCustomerPortalLink.tsx new file mode 100644 index 0000000..35e556a --- /dev/null +++ b/frontend/src/components/hosted/StripeCustomerPortalLink.tsx @@ -0,0 +1,48 @@ +import toast from "react-hot-toast"; +import { useAuthenticatedFetch } from "./useAuthenticatedFetch"; +import { addEvent } from "../../lib/analytics"; +import { SAAS_BACKEND_URL } from "../../config"; +import { PortalSessionResponse } from "./types"; +import { forwardRef, useState } from "react"; +import Spinner from "../custom-ui/Spinner"; + +interface Props { + label: string; +} + +const StripeCustomerPortalLink = forwardRef( + ({ label, ...props }, ref) => { + const [isLoading, setIsLoading] = useState(false); + const authenticatedFetch = useAuthenticatedFetch(); + + const redirectToBillingPortal = async () => { + try { + setIsLoading(true); + const res: PortalSessionResponse = await authenticatedFetch( + SAAS_BACKEND_URL + "/payments/create_portal_session", + "POST" + ); + window.location.href = res.url; + } catch (e) { + toast.error( + "Error directing you to the billing portal. Please email support and we'll get it fixed right away." + ); + addEvent("StripeBillingPortalError"); + } finally { + setIsLoading(false); + } + }; + + return ( + +
+ {label} {isLoading && } +
+
+ ); + } +); + +StripeCustomerPortalLink.displayName = "StripeCustomerPortalLink"; + +export default StripeCustomerPortalLink; diff --git a/frontend/src/components/hosted/types.ts b/frontend/src/components/hosted/types.ts index 9d613fc..b8b17bd 100644 --- a/frontend/src/components/hosted/types.ts +++ b/frontend/src/components/hosted/types.ts @@ -6,3 +6,7 @@ export interface UserResponse { subscriber_tier: string; stripe_customer_id: string; } + +export interface PortalSessionResponse { + url: string; +} diff --git a/frontend/src/components/hosted/useAuthenticatedFetch.ts b/frontend/src/components/hosted/useAuthenticatedFetch.ts index 1abcaab..aabba72 100644 --- a/frontend/src/components/hosted/useAuthenticatedFetch.ts +++ b/frontend/src/components/hosted/useAuthenticatedFetch.ts @@ -2,6 +2,9 @@ import { useAuth } from "@clerk/clerk-react"; type FetchMethod = "GET" | "POST" | "PUT" | "DELETE"; +// Assumes that the backend is using JWTs for authentication +// and assumes JSON responses +// *If response code is not 200 OK or if there's any other error, throws an error export const useAuthenticatedFetch = () => { const { getToken } = useAuth();