🐛 (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({
|
||||
|
@ -10,7 +10,7 @@ import {
|
||||
} from 'models'
|
||||
import { GraphNavigation, Plan, PrismaClient, WorkspaceRole } from 'db'
|
||||
import { readFileSync } from 'fs'
|
||||
import { createFakeResults } from 'utils'
|
||||
import { injectFakeResults } from 'utils'
|
||||
import { encrypt } from 'utils/api'
|
||||
|
||||
const prisma = new PrismaClient()
|
||||
@ -221,7 +221,7 @@ export const importTypebotInDatabase = async (
|
||||
})
|
||||
}
|
||||
|
||||
export const createResults = createFakeResults(prisma)
|
||||
export const createResults = injectFakeResults(prisma)
|
||||
|
||||
export const createSmtpCredentials = (
|
||||
id: string,
|
||||
|
@ -1,18 +1,23 @@
|
||||
import { PrismaClient } from 'db'
|
||||
import path from 'path'
|
||||
import fs from 'fs'
|
||||
import { injectFakeResults } from 'utils'
|
||||
|
||||
require('dotenv').config({
|
||||
path: path.join(
|
||||
__dirname,
|
||||
process.env.NODE_ENV === 'production'
|
||||
? '.env.production'
|
||||
: process.env.NODE_ENV === 'staging'
|
||||
? '.env.staging'
|
||||
: '.env.local'
|
||||
process.env.NODE_ENV === 'staging' ? '.env.staging' : '.env.local'
|
||||
),
|
||||
})
|
||||
|
||||
const main = async () => {}
|
||||
const prisma = new PrismaClient()
|
||||
|
||||
const main = async () => {
|
||||
await injectFakeResults(prisma)({
|
||||
count: 150,
|
||||
typebotId: 'cl89sq4vb030109laivd9ck97',
|
||||
isChronological: false,
|
||||
idPrefix: 'batch2',
|
||||
})
|
||||
}
|
||||
|
||||
main().then()
|
||||
|
@ -7,9 +7,7 @@
|
||||
"scripts": {
|
||||
"start:local": "ts-node index.ts",
|
||||
"start:staging": "NODE_ENV=staging ts-node index.ts",
|
||||
"start:prod": "NODE_ENV=production ts-node index.ts",
|
||||
"start:workspaces:migration": "ts-node workspaceMigration.ts",
|
||||
"start:workspaces:migration:recover": "ts-node workspaceMigrationRecover.ts"
|
||||
"start:prod": "NODE_ENV=production ts-node index.ts"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "18.7.16",
|
||||
@ -17,6 +15,6 @@
|
||||
"models": "workspace:*",
|
||||
"ts-node": "^10.9.1",
|
||||
"typescript": "^4.8.3",
|
||||
"utils": "*"
|
||||
"utils": "workspace:*"
|
||||
}
|
||||
}
|
||||
|
@ -1,33 +0,0 @@
|
||||
import fs from 'fs'
|
||||
|
||||
export const prepareEmojis = () => {
|
||||
const emojiData = JSON.parse(fs.readFileSync('./emojiData.json', 'utf8'))
|
||||
const strippedEmojiData = {
|
||||
'Smileys & Emotion': emojiData['Smileys & Emotion'].map(
|
||||
(emoji: { emoji: any }) => emoji.emoji
|
||||
),
|
||||
'People & Body': emojiData['People & Body'].map(
|
||||
(emoji: { emoji: any }) => emoji.emoji
|
||||
),
|
||||
'Animals & Nature': emojiData['Animals & Nature'].map(
|
||||
(emoji: { emoji: any }) => emoji.emoji
|
||||
),
|
||||
'Food & Drink': emojiData['Food & Drink'].map(
|
||||
(emoji: { emoji: any }) => emoji.emoji
|
||||
),
|
||||
'Travel & Places': emojiData['Travel & Places'].map(
|
||||
(emoji: { emoji: any }) => emoji.emoji
|
||||
),
|
||||
Activities: emojiData['Activities'].map(
|
||||
(emoji: { emoji: any }) => emoji.emoji
|
||||
),
|
||||
Objects: emojiData['Objects'].map((emoji: { emoji: any }) => emoji.emoji),
|
||||
Symbols: emojiData['Symbols'].map((emoji: { emoji: any }) => emoji.emoji),
|
||||
Flags: emojiData['Flags'].map((emoji: { emoji: any }) => emoji.emoji),
|
||||
}
|
||||
fs.writeFileSync(
|
||||
'strippedEmojis.json',
|
||||
JSON.stringify(strippedEmojiData),
|
||||
'utf8'
|
||||
)
|
||||
}
|
@ -8,7 +8,7 @@ type CreateFakeResultsProps = {
|
||||
fakeStorage?: number
|
||||
}
|
||||
|
||||
export const createFakeResults =
|
||||
export const injectFakeResults =
|
||||
(prisma: PrismaClient) =>
|
||||
async ({
|
||||
count,
|
||||
|
2
pnpm-lock.yaml
generated
2
pnpm-lock.yaml
generated
@ -520,7 +520,7 @@ importers:
|
||||
models: workspace:*
|
||||
ts-node: ^10.9.1
|
||||
typescript: ^4.8.3
|
||||
utils: '*'
|
||||
utils: workspace:*
|
||||
devDependencies:
|
||||
'@types/node': 18.7.16
|
||||
db: link:../db
|
||||
|
Reference in New Issue
Block a user