2
0

🐛 (stripe) Fix plan update and management

This commit is contained in:
Baptiste Arnaud
2022-09-20 19:15:47 +02:00
committed by Baptiste Arnaud
parent f83e0efea2
commit 6384a3adae
12 changed files with 99 additions and 105 deletions

View File

@ -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">

View File

@ -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,

View File

@ -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 }
}

View File

@ -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

View File

@ -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: {

View File

@ -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({

View File

@ -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,

View File

@ -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()

View File

@ -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:*"
}
}

View File

@ -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'
)
}

View File

@ -8,7 +8,7 @@ type CreateFakeResultsProps = {
fakeStorage?: number
}
export const createFakeResults =
export const injectFakeResults =
(prisma: PrismaClient) =>
async ({
count,

2
pnpm-lock.yaml generated
View File

@ -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