🐛 (stripe) Fix plan update and management
This commit is contained in:
committed by
Baptiste Arnaud
parent
f83e0efea2
commit
6384a3adae
@ -7,6 +7,7 @@ import {
|
||||
Button,
|
||||
Heading,
|
||||
} from '@chakra-ui/react'
|
||||
import { useToast } from 'components/shared/hooks/useToast'
|
||||
import { PlanTag } from 'components/shared/PlanTag'
|
||||
import { Plan } from 'db'
|
||||
import React, { useState } from 'react'
|
||||
@ -26,38 +27,48 @@ export const CurrentSubscriptionContent = ({
|
||||
const [isCancelling, setIsCancelling] = useState(false)
|
||||
const [isRedirectingToBillingPortal, setIsRedirectingToBillingPortal] =
|
||||
useState(false)
|
||||
const { showToast } = useToast()
|
||||
|
||||
const cancelSubscription = async () => {
|
||||
if (!stripeId) return
|
||||
setIsCancelling(true)
|
||||
await cancelSubscriptionQuery(stripeId)
|
||||
const { error } = await cancelSubscriptionQuery(stripeId)
|
||||
if (error) {
|
||||
showToast({ description: error.message })
|
||||
return
|
||||
}
|
||||
onCancelSuccess()
|
||||
setIsCancelling(false)
|
||||
}
|
||||
|
||||
const isSubscribed = (plan === Plan.STARTER || plan === Plan.PRO) && stripeId
|
||||
|
||||
if (isCancelling) return <Spinner colorScheme="gray" />
|
||||
return (
|
||||
<Stack gap="2">
|
||||
<Heading fontSize="3xl">Subscription</Heading>
|
||||
<HStack>
|
||||
<Text>Current workspace subscription: </Text>
|
||||
<PlanTag plan={plan} />
|
||||
{isSubscribed && (
|
||||
<Link
|
||||
as="button"
|
||||
color="gray.500"
|
||||
textDecor="underline"
|
||||
fontSize="sm"
|
||||
onClick={cancelSubscription}
|
||||
>
|
||||
Cancel my subscription
|
||||
</Link>
|
||||
{isCancelling ? (
|
||||
<Spinner color="gray.500" size="xs" />
|
||||
) : (
|
||||
<>
|
||||
<PlanTag plan={plan} />
|
||||
{isSubscribed && (
|
||||
<Link
|
||||
as="button"
|
||||
color="gray.500"
|
||||
textDecor="underline"
|
||||
fontSize="sm"
|
||||
onClick={cancelSubscription}
|
||||
>
|
||||
Cancel my subscription
|
||||
</Link>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</HStack>
|
||||
|
||||
{isSubscribed && (
|
||||
{isSubscribed && !isCancelling && (
|
||||
<>
|
||||
<Stack gap="1">
|
||||
<Text fontSize="sm">
|
||||
|
@ -35,7 +35,7 @@ export const ChangePlanForm = () => {
|
||||
selectedStorageLimitIndex === undefined
|
||||
)
|
||||
return
|
||||
await pay({
|
||||
const response = await pay({
|
||||
stripeId: workspace.stripeId ?? undefined,
|
||||
user,
|
||||
plan,
|
||||
@ -43,6 +43,10 @@ export const ChangePlanForm = () => {
|
||||
additionalChats: selectedChatsLimitIndex,
|
||||
additionalStorage: selectedStorageLimitIndex,
|
||||
})
|
||||
if (typeof response === 'object' && response?.error) {
|
||||
showToast({ description: response.error.message })
|
||||
return
|
||||
}
|
||||
refreshCurrentSubscriptionInfo({
|
||||
additionalChatsIndex: selectedChatsLimitIndex,
|
||||
additionalStorageIndex: selectedStorageLimitIndex,
|
||||
|
@ -20,7 +20,7 @@ type UpgradeProps = {
|
||||
export const pay = async ({
|
||||
stripeId,
|
||||
...props
|
||||
}: UpgradeProps): Promise<{ newPlan: Plan } | undefined | void> =>
|
||||
}: UpgradeProps): Promise<{ newPlan?: Plan; error?: Error } | void> =>
|
||||
isDefined(stripeId)
|
||||
? updatePlan({ ...props, stripeId })
|
||||
: redirectToCheckout(props)
|
||||
@ -31,13 +31,13 @@ export const updatePlan = async ({
|
||||
workspaceId,
|
||||
additionalChats,
|
||||
additionalStorage,
|
||||
}: Omit<UpgradeProps, 'user'>): Promise<{ newPlan: Plan } | undefined> => {
|
||||
}: Omit<UpgradeProps, 'user'>): Promise<{ newPlan?: Plan; error?: Error }> => {
|
||||
const { data, error } = await sendRequest<{ message: string }>({
|
||||
method: 'PUT',
|
||||
url: '/api/stripe/subscription',
|
||||
body: { workspaceId, plan, stripeId, additionalChats, additionalStorage },
|
||||
})
|
||||
if (error || !data) return
|
||||
if (error || !data) return { error }
|
||||
return { newPlan: plan }
|
||||
}
|
||||
|
||||
|
@ -18,6 +18,7 @@ import { SupportBubble } from 'components/shared/SupportBubble'
|
||||
import { WorkspaceContext } from 'contexts/WorkspaceContext'
|
||||
import { toTitleCase } from 'utils'
|
||||
import { Session } from 'next-auth'
|
||||
import { Plan } from 'db'
|
||||
|
||||
const { ToastContainer, toast } = createStandaloneToast(customTheme)
|
||||
|
||||
@ -35,7 +36,14 @@ const App = ({
|
||||
}, [pathname])
|
||||
|
||||
useEffect(() => {
|
||||
displayStripeCallbackMessage(query.stripe?.toString(), toast)
|
||||
const newPlan = query.stripe?.toString()
|
||||
if (newPlan === Plan.STARTER || newPlan === Plan.PRO)
|
||||
toast({
|
||||
position: 'bottom-right',
|
||||
status: 'success',
|
||||
title: 'Upgrade success!',
|
||||
description: `Workspace upgraded to ${toTitleCase(status)} 🎉`,
|
||||
})
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [isReady])
|
||||
|
||||
@ -68,19 +76,4 @@ const App = ({
|
||||
)
|
||||
}
|
||||
|
||||
const displayStripeCallbackMessage = (
|
||||
status: string | undefined,
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
toast: any
|
||||
) => {
|
||||
if (status && ['pro', 'team'].includes(status)) {
|
||||
toast({
|
||||
position: 'bottom-right',
|
||||
status: 'success',
|
||||
title: 'Upgrade success!',
|
||||
description: `Workspace upgraded to ${toTitleCase(status)} 🎉`,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
export default App
|
||||
|
@ -54,12 +54,12 @@ const getSubscriptionDetails =
|
||||
})
|
||||
return {
|
||||
additionalChatsIndex:
|
||||
subscriptions.data[0].items.data.find(
|
||||
subscriptions.data[0]?.items.data.find(
|
||||
(item) =>
|
||||
item.price.id === process.env.STRIPE_ADDITIONAL_CHATS_PRICE_ID
|
||||
)?.quantity ?? 0,
|
||||
additionalStorageIndex:
|
||||
subscriptions.data[0].items.data.find(
|
||||
subscriptions.data[0]?.items.data.find(
|
||||
(item) =>
|
||||
item.price.id === process.env.STRIPE_ADDITIONAL_STORAGE_PRICE_ID
|
||||
)?.quantity ?? 0,
|
||||
@ -100,33 +100,34 @@ const createCheckoutSession = (req: NextApiRequest) => {
|
||||
}
|
||||
|
||||
const updateSubscription = async (req: NextApiRequest) => {
|
||||
const { customerId, plan, workspaceId, additionalChats, additionalStorage } =
|
||||
(typeof req.body === 'string' ? JSON.parse(req.body) : req.body) as {
|
||||
customerId: string
|
||||
workspaceId: string
|
||||
additionalChats: number
|
||||
additionalStorage: number
|
||||
plan: 'STARTER' | 'PRO'
|
||||
}
|
||||
const { stripeId, plan, workspaceId, additionalChats, additionalStorage } = (
|
||||
typeof req.body === 'string' ? JSON.parse(req.body) : req.body
|
||||
) as {
|
||||
stripeId: string
|
||||
workspaceId: string
|
||||
additionalChats: number
|
||||
additionalStorage: number
|
||||
plan: 'STARTER' | 'PRO'
|
||||
}
|
||||
if (!process.env.STRIPE_SECRET_KEY)
|
||||
throw Error('STRIPE_SECRET_KEY var is missing')
|
||||
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY, {
|
||||
apiVersion: '2022-08-01',
|
||||
})
|
||||
const { data } = await stripe.subscriptions.list({
|
||||
customer: customerId,
|
||||
customer: stripeId,
|
||||
})
|
||||
const subscription = data[0]
|
||||
const currentStarterPlanItemId = subscription.items.data.find(
|
||||
const subscription = data[0] as Stripe.Subscription | undefined
|
||||
const currentStarterPlanItemId = subscription?.items.data.find(
|
||||
(item) => item.price.id === process.env.STRIPE_STARTER_PRICE_ID
|
||||
)?.id
|
||||
const currentProPlanItemId = subscription.items.data.find(
|
||||
const currentProPlanItemId = subscription?.items.data.find(
|
||||
(item) => item.price.id === process.env.STRIPE_PRO_PRICE_ID
|
||||
)?.id
|
||||
const currentAdditionalChatsItemId = subscription.items.data.find(
|
||||
const currentAdditionalChatsItemId = subscription?.items.data.find(
|
||||
(item) => item.price.id === process.env.STRIPE_ADDITIONAL_CHATS_PRICE_ID
|
||||
)?.id
|
||||
const currentAdditionalStorageItemId = subscription.items.data.find(
|
||||
const currentAdditionalStorageItemId = subscription?.items.data.find(
|
||||
(item) => item.price.id === process.env.STRIPE_ADDITIONAL_STORAGE_PRICE_ID
|
||||
)?.id
|
||||
const items = [
|
||||
@ -155,9 +156,18 @@ const updateSubscription = async (req: NextApiRequest) => {
|
||||
deleted: additionalStorage === 0,
|
||||
},
|
||||
].filter(isDefined)
|
||||
await stripe.subscriptions.update(subscription.id, {
|
||||
items,
|
||||
})
|
||||
|
||||
if (subscription) {
|
||||
await stripe.subscriptions.update(subscription.id, {
|
||||
items,
|
||||
})
|
||||
} else {
|
||||
await stripe.subscriptions.create({
|
||||
customer: stripeId,
|
||||
items,
|
||||
})
|
||||
}
|
||||
|
||||
await prisma.workspace.update({
|
||||
where: { id: workspaceId },
|
||||
data: {
|
||||
@ -187,7 +197,10 @@ const cancelSubscription =
|
||||
const existingSubscription = await stripe.subscriptions.list({
|
||||
customer: workspace.stripeId,
|
||||
})
|
||||
await stripe.subscriptions.del(existingSubscription.data[0].id)
|
||||
const currentSubscriptionId = existingSubscription.data[0]?.id
|
||||
if (currentSubscriptionId)
|
||||
await stripe.subscriptions.del(currentSubscriptionId)
|
||||
|
||||
await prisma.workspace.update({
|
||||
where: { id: workspace.id },
|
||||
data: {
|
||||
|
@ -18,7 +18,7 @@ import {
|
||||
Workspace,
|
||||
} from 'db'
|
||||
import { readFileSync } from 'fs'
|
||||
import { createFakeResults } from 'utils'
|
||||
import { injectFakeResults } from 'utils'
|
||||
import { encrypt } from 'utils/api'
|
||||
import Stripe from 'stripe'
|
||||
|
||||
@ -75,7 +75,10 @@ export const addSubscriptionToWorkspace = async (
|
||||
customer: stripeId,
|
||||
items,
|
||||
default_payment_method: paymentId,
|
||||
currency: 'usd',
|
||||
currency: 'eur',
|
||||
})
|
||||
await stripe.customers.update(stripeId, {
|
||||
invoice_settings: { default_payment_method: paymentId },
|
||||
})
|
||||
await prisma.workspace.update({
|
||||
where: { id: workspaceId },
|
||||
@ -264,7 +267,7 @@ export const updateUser = (data: Partial<User>) =>
|
||||
},
|
||||
})
|
||||
|
||||
export const createResults = createFakeResults(prisma)
|
||||
export const createResults = injectFakeResults(prisma)
|
||||
|
||||
export const createFolder = (workspaceId: string, name: string) =>
|
||||
prisma.dashboardFolder.create({
|
||||
|
Reference in New Issue
Block a user