diff --git a/.env.example b/.env.example index 71e2da2a6..33d5e780a 100644 --- a/.env.example +++ b/.env.example @@ -23,6 +23,11 @@ GOOGLE_CLIENT_SECRET= FACEBOOK_CLIENT_ID= FACEBOOK_CLIENT_SECRET= +# (Optional) Subscription Payment +NEXT_PUBLIC_STRIPE_PUBLIC_KEY= +STRIPE_SECRET_KEY= +STRIPE_WEBHOOK_SECRET= + # Used for uploading images, videos, etc... S3_UPLOAD_KEY= S3_UPLOAD_SECRET= diff --git a/apps/builder/assets/icons.tsx b/apps/builder/assets/icons.tsx index b217d7aa7..2675f8f2e 100644 --- a/apps/builder/assets/icons.tsx +++ b/apps/builder/assets/icons.tsx @@ -191,3 +191,12 @@ export const EditIcon = (props: IconProps) => ( ) + +export const UploadIcon = (props: IconProps) => ( + + + + + + +) diff --git a/apps/builder/components/HOC/withUser.tsx b/apps/builder/components/HOC/withUser.tsx deleted file mode 100644 index 5526e3484..000000000 --- a/apps/builder/components/HOC/withUser.tsx +++ /dev/null @@ -1,20 +0,0 @@ -import { useSession } from 'next-auth/react' -import { useRouter } from 'next/router' -import { useEffect } from 'react' - -const withAuth = - (WrappedComponent: (props: any) => JSX.Element) => - (props: JSX.IntrinsicAttributes) => { - const router = useRouter() - const { status } = useSession() - - useEffect(() => { - if (!router.isReady) return - if (status === 'loading') return - if (status === 'unauthenticated') router.replace('/signin') - }, [status, router]) - - return - } - -export default withAuth diff --git a/apps/builder/components/account/AccountHeader.tsx b/apps/builder/components/account/AccountHeader.tsx new file mode 100644 index 000000000..e67f13561 --- /dev/null +++ b/apps/builder/components/account/AccountHeader.tsx @@ -0,0 +1,24 @@ +import { Flex } from '@chakra-ui/react' +import { TypebotLogo } from 'assets/logos' +import { NextChakraLink } from 'components/nextChakra/NextChakraLink' +import React from 'react' + +export const AccountHeader = () => ( + + + + + + + +) diff --git a/apps/builder/components/account/BillingSection.tsx b/apps/builder/components/account/BillingSection.tsx new file mode 100644 index 000000000..1bf1d002c --- /dev/null +++ b/apps/builder/components/account/BillingSection.tsx @@ -0,0 +1,27 @@ +import { Stack, Heading, HStack, Button, Text } from '@chakra-ui/react' +import { NextChakraLink } from 'components/nextChakra/NextChakraLink' +import { useUser } from 'contexts/UserContext' +import React from 'react' +import { SubscriptionTag } from './SubscriptionTag' + +export const BillingSection = () => { + const { user } = useUser() + return ( + + + Billing + + + + Your subscription + + + {user?.stripeId && ( + + )} + + + ) +} diff --git a/apps/builder/components/account/PersonalInfoForm.tsx b/apps/builder/components/account/PersonalInfoForm.tsx new file mode 100644 index 000000000..2ec0cc04c --- /dev/null +++ b/apps/builder/components/account/PersonalInfoForm.tsx @@ -0,0 +1,116 @@ +import { + Stack, + Heading, + HStack, + Avatar, + Button, + FormControl, + FormLabel, + Input, + Tooltip, + Flex, + Text, +} from '@chakra-ui/react' +import { UploadIcon } from 'assets/icons' +import { UploadButton } from 'components/shared/buttons/UploadButton' +import { useUser } from 'contexts/UserContext' +import React, { ChangeEvent, useState } from 'react' +import { uploadFile } from 'services/utils' + +export const PersonalInfoForm = () => { + const { + user, + updateUser, + saveUser, + hasUnsavedChanges, + isSaving, + isOAuthProvider, + } = useUser() + const [reloadParam, setReloadParam] = useState('') + const [isUploading, setIsUploading] = useState(false) + + const handleFileChange = async (file: File) => { + setIsUploading(true) + const { url } = await uploadFile(file, `${user?.id}/avatar`) + setReloadParam(Date.now().toString()) + updateUser({ image: url }) + setIsUploading(false) + } + + const handleNameChange = (e: ChangeEvent) => { + updateUser({ name: e.target.value }) + } + + const handleEmailChange = (e: ChangeEvent) => { + updateUser({ email: e.target.value }) + } + + return ( + + + Personal info + + + + + + } + isLoading={isUploading} + onUploadChange={handleFileChange} + > + Change photo + + + .jpg or.png, max 1MB + + + + + + Name + + + + + + Email address + + + + + + {hasUnsavedChanges && ( + + + + )} + + + ) +} diff --git a/apps/builder/components/account/SubscriptionTag.tsx b/apps/builder/components/account/SubscriptionTag.tsx new file mode 100644 index 000000000..fcd72a1ec --- /dev/null +++ b/apps/builder/components/account/SubscriptionTag.tsx @@ -0,0 +1,22 @@ +import { Tag } from '@chakra-ui/react' +import { Plan } from 'db' + +export const SubscriptionTag = ({ plan }: { plan?: Plan }) => { + switch (plan) { + case Plan.FREE: { + return Free plan + } + case Plan.LIFETIME: { + return Lifetime plan + } + case Plan.OFFERED: { + return Offered + } + case Plan.PRO: { + return Pro plan + } + default: { + return Free plan + } + } +} diff --git a/apps/builder/components/board/graph/BlockNode/BlockNode.tsx b/apps/builder/components/board/graph/BlockNode/BlockNode.tsx index 1fbd2b1b2..86d200cd3 100644 --- a/apps/builder/components/board/graph/BlockNode/BlockNode.tsx +++ b/apps/builder/components/board/graph/BlockNode/BlockNode.tsx @@ -10,7 +10,7 @@ import { Block, StartBlock } from 'bot-engine' import { useGraph } from 'contexts/GraphContext' import { useDnd } from 'contexts/DndContext' import { StepsList } from './StepsList' -import { isNotDefined } from 'services/utils' +import { isDefined } from 'services/utils' import { useTypebot } from 'contexts/TypebotContext' import { ContextMenu } from 'components/shared/ContextMenu' import { BlockNodeContextMenu } from './BlockNodeContextMenu' @@ -34,7 +34,7 @@ export const BlockNode = ({ block }: { block: Block | StartBlock }) => { useEffect(() => { setIsConnecting( connectingIds?.target?.blockId === block.id && - isNotDefined(connectingIds.target?.stepId) + !isDefined(connectingIds.target?.stepId) ) }, [block.id, connectingIds]) diff --git a/apps/builder/components/dashboard/DashboardHeader.tsx b/apps/builder/components/dashboard/DashboardHeader.tsx index b4ff8d894..bddae052f 100644 --- a/apps/builder/components/dashboard/DashboardHeader.tsx +++ b/apps/builder/components/dashboard/DashboardHeader.tsx @@ -12,13 +12,13 @@ import { Skeleton, } from '@chakra-ui/react' import { TypebotLogo } from 'assets/logos' -import { useUser } from 'services/user' import { NextChakraLink } from 'components/nextChakra/NextChakraLink' import { LogOutIcon, SettingsIcon } from 'assets/icons' import { signOut } from 'next-auth/react' +import { useUser } from 'contexts/UserContext' export const DashboardHeader = () => { - const user = useUser() + const { user } = useUser() const handleLogOut = () => { signOut() diff --git a/apps/builder/components/shared/buttons/UploadButton.tsx b/apps/builder/components/shared/buttons/UploadButton.tsx new file mode 100644 index 000000000..b721549ee --- /dev/null +++ b/apps/builder/components/shared/buttons/UploadButton.tsx @@ -0,0 +1,34 @@ +import { Button, ButtonProps, chakra } from '@chakra-ui/react' +import React, { ChangeEvent } from 'react' + +type UploadButtonProps = { onUploadChange: (file: File) => void } & ButtonProps + +export const UploadButton = ({ + onUploadChange, + ...props +}: UploadButtonProps) => { + const handleInputChange = (e: ChangeEvent) => { + if (!e.target?.files) return + onUploadChange(e.target.files[0]) + } + return ( + <> + + + + ) +} diff --git a/apps/builder/contexts/UserContext.tsx b/apps/builder/contexts/UserContext.tsx new file mode 100644 index 000000000..53d70b5ab --- /dev/null +++ b/apps/builder/contexts/UserContext.tsx @@ -0,0 +1,100 @@ +import { User } from 'db' +import { useSession } from 'next-auth/react' +import { useRouter } from 'next/router' +import { + createContext, + ReactNode, + useContext, + useEffect, + useMemo, + useState, +} from 'react' +import { isDefined } from 'services/utils' +import { updateUser as updateUserInDb } from 'services/user' +import { useToast } from '@chakra-ui/react' +import { deepEqual } from 'fast-equals' + +const userContext = createContext<{ + user?: User + isLoading: boolean + isSaving: boolean + hasUnsavedChanges: boolean + isOAuthProvider: boolean + updateUser: (newUser: Partial) => void + saveUser: () => void + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + //@ts-ignore +}>({}) + +export const UserContext = ({ children }: { children: ReactNode }) => { + const router = useRouter() + const { data: session, status } = useSession() + + const [user, setUser] = useState() + const [isSaving, setIsSaving] = useState(false) + const isOAuthProvider = useMemo( + () => (session?.providerType as boolean | undefined) ?? false, + [session?.providerType] + ) + + const hasUnsavedChanges = useMemo( + () => !deepEqual(session?.user, user), + [session?.user, user] + ) + + const toast = useToast({ + position: 'top-right', + status: 'error', + }) + + useEffect(() => { + if (isDefined(user) || !isDefined(session)) return + + setUser(session.user as User) + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [session]) + + useEffect(() => { + if (!router.isReady) return + if (status === 'loading') return + if (status === 'unauthenticated') router.replace('/signin') + }, [status, router]) + + const updateUser = (newUser: Partial) => { + if (!isDefined(user)) return + setUser({ ...user, ...newUser }) + } + + const saveUser = async () => { + if (!isDefined(user)) return + setIsSaving(true) + const { error } = await updateUserInDb(user.id, user) + if (error) toast({ title: error.name, description: error.message }) + await fetch('/api/auth/session?update') + reloadSession() + setIsSaving(false) + } + + return ( + + {children} + + ) +} + +const reloadSession = () => { + const event = new Event('visibilitychange') + document.dispatchEvent(event) +} + +export const useUser = () => useContext(userContext) diff --git a/apps/builder/layouts/account/AccountContent.tsx b/apps/builder/layouts/account/AccountContent.tsx new file mode 100644 index 000000000..a1dd9aab1 --- /dev/null +++ b/apps/builder/layouts/account/AccountContent.tsx @@ -0,0 +1,34 @@ +import { Flex, Stack, Heading, Divider, Button } from '@chakra-ui/react' +import { ChevronLeftIcon } from 'assets/icons' +import { NextChakraLink } from 'components/nextChakra/NextChakraLink' +import React from 'react' +import { PersonalInfoForm } from 'components/account/PersonalInfoForm' +import { BillingSection } from 'components/account/BillingSection' + +export const AccountContent = () => { + return ( + + + + + + + + Account Settings + + + + + + + + ) +} diff --git a/apps/builder/package.json b/apps/builder/package.json index 3307f652f..737a57f62 100644 --- a/apps/builder/package.json +++ b/apps/builder/package.json @@ -24,6 +24,7 @@ "@udecode/plate-link": "^9.0.0", "@udecode/plate-ui-link": "^9.0.0", "@udecode/plate-ui-toolbar": "^9.0.0", + "aws-sdk": "^2.1048.0", "bot-engine": "*", "db": "*", "fast-equals": "^2.0.4", @@ -31,6 +32,7 @@ "framer-motion": "^4", "htmlparser2": "^7.2.0", "kbar": "^0.1.0-beta.24", + "micro-cors": "^0.1.1", "next": "^12.0.7", "next-auth": "beta", "nodemailer": "^6.7.2", @@ -44,6 +46,7 @@ "slate-history": "^0.66.0", "slate-hyperscript": "^0.67.0", "slate-react": "^0.72.1", + "stripe": "^8.195.0", "styled-components": "^5.3.3", "svg-round-corners": "^0.3.0", "swr": "^1.1.1", @@ -51,6 +54,7 @@ }, "devDependencies": { "@testing-library/cypress": "^8.0.2", + "@types/micro-cors": "^0.1.2", "@types/node": "^16.11.9", "@types/nprogress": "^0.2.0", "@types/react": "^17.0.37", diff --git a/apps/builder/pages/account.tsx b/apps/builder/pages/account.tsx new file mode 100644 index 000000000..a6a07d5b2 --- /dev/null +++ b/apps/builder/pages/account.tsx @@ -0,0 +1,18 @@ +import { Stack } from '@chakra-ui/react' +import { AccountHeader } from 'components/account/AccountHeader' +import { Seo } from 'components/Seo' +import { UserContext } from 'contexts/UserContext' +import { AccountContent } from 'layouts/account/AccountContent' + +const AccountSubscriptionPage = () => { + return ( + + {' '} + + + + + + ) +} +export default AccountSubscriptionPage diff --git a/apps/builder/pages/api/auth/[...nextauth].ts b/apps/builder/pages/api/auth/[...nextauth].ts index ff9055089..84fd361aa 100644 --- a/apps/builder/pages/api/auth/[...nextauth].ts +++ b/apps/builder/pages/api/auth/[...nextauth].ts @@ -1,4 +1,4 @@ -import NextAuth from 'next-auth' +import NextAuth, { NextAuthOptions } from 'next-auth' import { PrismaAdapter } from '@next-auth/prisma-adapter' import EmailProvider from 'next-auth/providers/email' import GitHubProvider from 'next-auth/providers/github' @@ -8,6 +8,7 @@ import CredentialsProvider from 'next-auth/providers/credentials' import prisma from 'libs/prisma' import { Provider } from 'next-auth/providers' import { User } from 'db' +import { NextApiRequest, NextApiResponse } from 'next' const providers: Provider[] = [ EmailProvider({ @@ -67,7 +68,7 @@ if (process.env.NODE_ENV !== 'production') }) ) -export default NextAuth({ +const createOptions = (req: NextApiRequest): NextAuthOptions => ({ adapter: PrismaAdapter(prisma), secret: process.env.SECRET, providers, @@ -75,13 +76,25 @@ export default NextAuth({ strategy: process.env.NODE_ENV === 'production' ? 'database' : 'jwt', }, callbacks: { - jwt: async ({ token, user }) => { - user && (token.user = user) + jwt: async ({ token, user, account }) => { + if (req.url === '/api/auth/session?update' && token.user) { + token.user = await prisma.user.findUnique({ + where: { id: (token.user as User).id }, + }) + } else if (user) { + token.user = user + } + account?.type && (token.providerType = account?.type) return token }, session: async ({ session, token, user }) => { token?.user ? (session.user = token.user as User) : (session.user = user) - return session + return { ...session, providerType: token.providerType } }, }, }) + +const handler = (req: NextApiRequest, res: NextApiResponse) => { + NextAuth(req, res, createOptions(req)) +} +export default handler diff --git a/apps/builder/pages/api/storage/upload-url.ts b/apps/builder/pages/api/storage/upload-url.ts new file mode 100644 index 000000000..11a4b8c41 --- /dev/null +++ b/apps/builder/pages/api/storage/upload-url.ts @@ -0,0 +1,46 @@ +import aws from 'aws-sdk' +import { NextApiRequest, NextApiResponse } from 'next' +import { getSession } from 'next-auth/react' +import { methodNotAllowed } from 'services/api/utils' + +const maxUploadFileSize = 10485760 // 10 MB +const handler = async ( + req: NextApiRequest, + res: NextApiResponse +): Promise => { + try { + res.setHeader('Access-Control-Allow-Origin', '*') + if (req.method === 'GET') { + const session = await getSession({ req }) + if (!session) { + res.status(401) + return + } + aws.config.update({ + accessKeyId: process.env.S3_UPLOAD_KEY, + secretAccessKey: process.env.S3_UPLOAD_SECRET, + region: process.env.S3_UPLOAD_REGION, + signatureVersion: 'v4', + }) + + const s3 = new aws.S3() + const post = s3.createPresignedPost({ + Bucket: process.env.S3_UPLOAD_BUCKET, + Fields: { + ACL: 'public-read', + key: req.query.key, + 'Content-Type': req.query.fileType, + }, + Expires: 120, // seconds + Conditions: [['content-length-range', 0, maxUploadFileSize]], + }) + + return res.status(200).json(post) + } + return methodNotAllowed(res) + } catch (err) { + console.log(err) + } +} + +export default handler diff --git a/apps/builder/pages/api/stripe/checkout.ts b/apps/builder/pages/api/stripe/checkout.ts new file mode 100644 index 000000000..968d44381 --- /dev/null +++ b/apps/builder/pages/api/stripe/checkout.ts @@ -0,0 +1,35 @@ +import { NextApiRequest, NextApiResponse } from 'next' +import { methodNotAllowed } from 'services/api/utils' +import Stripe from 'stripe' + +const usdPriceIdTest = 'price_1Jc4TQKexUFvKTWyGvsH4Ff5' +const createCheckoutSession = async ( + req: NextApiRequest, + res: NextApiResponse +) => { + if (req.method === 'POST') { + if (!process.env.STRIPE_SECRET_KEY) + throw Error('STRIPE_SECRET_KEY var is missing') + const stripe = new Stripe(process.env.STRIPE_SECRET_KEY, { + apiVersion: '2020-08-27', + }) + const { email } = req.body + const session = await stripe.checkout.sessions.create({ + success_url: `${req.headers.origin}/typebots?stripe=success`, + cancel_url: `${req.headers.origin}/typebots?stripe=cancel`, + automatic_tax: { enabled: true }, + allow_promotion_codes: true, + customer_email: email, + line_items: [ + { + price: usdPriceIdTest, + quantity: 1, + }, + ], + }) + res.status(201).json(session) + } + return methodNotAllowed(res) +} + +export default createCheckoutSession diff --git a/apps/builder/pages/api/stripe/webhook.ts b/apps/builder/pages/api/stripe/webhook.ts new file mode 100644 index 000000000..6c9e8979f --- /dev/null +++ b/apps/builder/pages/api/stripe/webhook.ts @@ -0,0 +1,73 @@ +import { NextApiRequest, NextApiResponse } from 'next' +import { methodNotAllowed } from 'services/api/utils' +import Stripe from 'stripe' +import Cors from 'micro-cors' +import { buffer } from 'micro' +import prisma from 'libs/prisma' +import { Plan } from 'db' + +if (!process.env.STRIPE_SECRET_KEY || !process.env.STRIPE_WEBHOOK_SECRET) + throw new Error('STRIPE_SECRET_KEY or STRIPE_WEBHOOK_SECRET missing') +const stripe = new Stripe(process.env.STRIPE_SECRET_KEY, { + apiVersion: '2020-08-27', +}) + +const cors = Cors({ + allowMethods: ['POST', 'HEAD'], +}) + +const webhookSecret = process.env.STRIPE_WEBHOOK_SECRET as string + +export const config = { + api: { + bodyParser: false, + }, +} + +const webhookHandler = async (req: NextApiRequest, res: NextApiResponse) => { + if (req.method === 'POST') { + const buf = await buffer(req) + const sig = req.headers['stripe-signature'] + + if (!sig) return res.status(400).send(`stripe-signature is missing`) + try { + const event = stripe.webhooks.constructEvent( + buf.toString(), + sig.toString(), + webhookSecret + ) + switch (event.type) { + case 'checkout.session.completed': { + const session = event.data.object as Stripe.Checkout.Session + const { customer_email } = session + if (!customer_email) + return res.status(500).send(`customer_email not found`) + await prisma.user.update({ + where: { email: customer_email }, + data: { plan: Plan.PRO, stripeId: session.customer as string }, + }) + } + case 'customer.subscription.deleted': { + const subscription = event.data.object as Stripe.Subscription + await prisma.user.update({ + where: { + stripeId: subscription.customer as string, + }, + data: { + plan: Plan.FREE, + }, + }) + } + } + } catch (err) { + if (err instanceof Error) { + console.error(err) + return res.status(400).send(`Webhook Error: ${err.message}`) + } + } + } + return methodNotAllowed(res) +} + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export default cors(webhookHandler as any) diff --git a/apps/builder/pages/api/users/[id].ts b/apps/builder/pages/api/users/[id].ts new file mode 100644 index 000000000..34a9b318d --- /dev/null +++ b/apps/builder/pages/api/users/[id].ts @@ -0,0 +1,24 @@ +import prisma from 'libs/prisma' +import { NextApiRequest, NextApiResponse } from 'next' +import { getSession } from 'next-auth/react' +import { methodNotAllowed } from 'services/api/utils' + +const handler = async (req: NextApiRequest, res: NextApiResponse) => { + const session = await getSession({ req }) + + if (!session?.user) + return res.status(401).json({ message: 'Not authenticated' }) + + const id = req.query.id.toString() + if (req.method === 'PUT') { + const data = JSON.parse(req.body) + const typebots = await prisma.user.update({ + where: { id }, + data, + }) + return res.send({ typebots }) + } + return methodNotAllowed(res) +} + +export default handler diff --git a/apps/builder/pages/typebots.tsx b/apps/builder/pages/typebots.tsx index 370b784f8..f1fad9c98 100644 --- a/apps/builder/pages/typebots.tsx +++ b/apps/builder/pages/typebots.tsx @@ -1,18 +1,20 @@ -import withAuth from 'components/HOC/withUser' import React from 'react' import { Stack } from '@chakra-ui/layout' import { DashboardHeader } from 'components/dashboard/DashboardHeader' import { Seo } from 'components/Seo' import { FolderContent } from 'components/dashboard/FolderContent' +import { UserContext } from 'contexts/UserContext' const DashboardPage = () => { return ( - + - - - + + + + + ) } -export default withAuth(DashboardPage) +export default DashboardPage diff --git a/apps/builder/pages/typebots/[id]/edit.tsx b/apps/builder/pages/typebots/[id]/edit.tsx index 1ceda0fab..6ef7f2a71 100644 --- a/apps/builder/pages/typebots/[id]/edit.tsx +++ b/apps/builder/pages/typebots/[id]/edit.tsx @@ -1,6 +1,5 @@ import { Flex } from '@chakra-ui/layout' import { Board } from 'components/board/Board' -import withAuth from 'components/HOC/withUser' import { Seo } from 'components/Seo' import { TypebotHeader } from 'components/shared/TypebotHeader' import { EditorContext } from 'contexts/EditorContext' @@ -11,25 +10,28 @@ import { KBarProvider } from 'kbar' import React from 'react' import { actions } from 'libs/kbar' import { KBar } from 'components/shared/KBar' +import { UserContext } from 'contexts/UserContext' const TypebotEditPage = () => { const { query } = useRouter() return ( - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + ) } -export default withAuth(TypebotEditPage) +export default TypebotEditPage diff --git a/apps/builder/pages/typebots/[id]/results.tsx b/apps/builder/pages/typebots/[id]/results.tsx index f2594d869..b6aa38d9b 100644 --- a/apps/builder/pages/typebots/[id]/results.tsx +++ b/apps/builder/pages/typebots/[id]/results.tsx @@ -1,23 +1,25 @@ import { Flex } from '@chakra-ui/layout' -import withAuth from 'components/HOC/withUser' import { ResultsContent } from 'layouts/results/ResultsContent' import { Seo } from 'components/Seo' import { TypebotHeader } from 'components/shared/TypebotHeader' import { TypebotContext } from 'contexts/TypebotContext' import { useRouter } from 'next/router' import React from 'react' +import { UserContext } from 'contexts/UserContext' const ResultsPage = () => { const { query } = useRouter() return ( - - - - - - - + + + + + + + + + ) } -export default withAuth(ResultsPage) +export default ResultsPage diff --git a/apps/builder/pages/typebots/[id]/results/analytics.tsx b/apps/builder/pages/typebots/[id]/results/analytics.tsx index 77ea466a6..f5119db0c 100644 --- a/apps/builder/pages/typebots/[id]/results/analytics.tsx +++ b/apps/builder/pages/typebots/[id]/results/analytics.tsx @@ -1,23 +1,25 @@ import { Flex } from '@chakra-ui/layout' -import withAuth from 'components/HOC/withUser' import { ResultsContent } from 'layouts/results/ResultsContent' import { Seo } from 'components/Seo' import { TypebotHeader } from 'components/shared/TypebotHeader' import { TypebotContext } from 'contexts/TypebotContext' import { useRouter } from 'next/router' import React from 'react' +import { UserContext } from 'contexts/UserContext' const AnalyticsPage = () => { const { query } = useRouter() return ( - - - - - - - + + + + + + + + + ) } -export default withAuth(AnalyticsPage) +export default AnalyticsPage diff --git a/apps/builder/pages/typebots/[id]/settings.tsx b/apps/builder/pages/typebots/[id]/settings.tsx index 4d2d486eb..11823edbc 100644 --- a/apps/builder/pages/typebots/[id]/settings.tsx +++ b/apps/builder/pages/typebots/[id]/settings.tsx @@ -1,23 +1,25 @@ import { Flex } from '@chakra-ui/layout' -import withAuth from 'components/HOC/withUser' import { Seo } from 'components/Seo' import { SettingsContent } from 'components/settings/SettingsContent' import { TypebotHeader } from 'components/shared/TypebotHeader' import { TypebotContext } from 'contexts/TypebotContext' +import { UserContext } from 'contexts/UserContext' import { useRouter } from 'next/router' import React from 'react' const SettingsPage = () => { const { query } = useRouter() return ( - - - - - - - + + + + + + + + + ) } -export default withAuth(SettingsPage) +export default SettingsPage diff --git a/apps/builder/pages/typebots/[id]/share.tsx b/apps/builder/pages/typebots/[id]/share.tsx index 74ed0239c..790215fd8 100644 --- a/apps/builder/pages/typebots/[id]/share.tsx +++ b/apps/builder/pages/typebots/[id]/share.tsx @@ -1,23 +1,25 @@ import { Flex } from '@chakra-ui/layout' -import withAuth from 'components/HOC/withUser' import { Seo } from 'components/Seo' import { ShareContent } from 'components/share/ShareContent' import { TypebotHeader } from 'components/shared/TypebotHeader' import { TypebotContext } from 'contexts/TypebotContext' +import { UserContext } from 'contexts/UserContext' import { useRouter } from 'next/router' import React from 'react' const SharePage = () => { const { query } = useRouter() return ( - - - - - - - + + + + + + + + + ) } -export default withAuth(SharePage) +export default SharePage diff --git a/apps/builder/pages/typebots/[id]/theme.tsx b/apps/builder/pages/typebots/[id]/theme.tsx index 691dd8c61..059191b1a 100644 --- a/apps/builder/pages/typebots/[id]/theme.tsx +++ b/apps/builder/pages/typebots/[id]/theme.tsx @@ -1,23 +1,25 @@ import { Flex } from '@chakra-ui/layout' -import withAuth from 'components/HOC/withUser' import { Seo } from 'components/Seo' import { TypebotHeader } from 'components/shared/TypebotHeader' import { ThemeContent } from 'components/theme/ThemeContent' import { TypebotContext } from 'contexts/TypebotContext' +import { UserContext } from 'contexts/UserContext' import { useRouter } from 'next/router' import React from 'react' const ThemePage = () => { const { query } = useRouter() return ( - - - - - - - + + + + + + + + + ) } -export default withAuth(ThemePage) +export default ThemePage diff --git a/apps/builder/pages/typebots/create.tsx b/apps/builder/pages/typebots/create.tsx index d57457df1..566850998 100644 --- a/apps/builder/pages/typebots/create.tsx +++ b/apps/builder/pages/typebots/create.tsx @@ -1,14 +1,13 @@ import React, { useState } from 'react' import { Button, Stack, useToast } from '@chakra-ui/react' -import { useUser } from 'services/user' import { useRouter } from 'next/router' import { Seo } from 'components/Seo' import { DashboardHeader } from 'components/dashboard/DashboardHeader' import { createTypebot } from 'services/typebots' -import withAuth from 'components/HOC/withUser' +import { UserContext, useUser } from 'contexts/UserContext' const TemplatesPage = () => { - const user = useUser() + const { user } = useUser() const router = useRouter() const [isLoading, setIsLoading] = useState(false) @@ -31,14 +30,20 @@ const TemplatesPage = () => { } return ( - + - - - + + + + + ) } -export default withAuth(TemplatesPage) +export default TemplatesPage diff --git a/apps/builder/pages/typebots/folders/[id].tsx b/apps/builder/pages/typebots/folders/[id].tsx index c04416ef7..206ca604b 100644 --- a/apps/builder/pages/typebots/folders/[id].tsx +++ b/apps/builder/pages/typebots/folders/[id].tsx @@ -1,4 +1,3 @@ -import withAuth from 'components/HOC/withUser' import React from 'react' import { Flex, Stack } from '@chakra-ui/layout' import { DashboardHeader } from 'components/dashboard/DashboardHeader' @@ -7,6 +6,7 @@ import { FolderContent } from 'components/dashboard/FolderContent' import { useRouter } from 'next/router' import { useFolderContent } from 'services/folders' import { Spinner, useToast } from '@chakra-ui/react' +import { UserContext } from 'contexts/UserContext' const FolderPage = () => { const router = useRouter() @@ -27,18 +27,20 @@ const FolderPage = () => { }) return ( - + - - {!folder ? ( - - - - ) : ( - - )} - + + + {!folder ? ( + + + + ) : ( + + )} + + ) } -export default withAuth(FolderPage) +export default FolderPage diff --git a/apps/builder/services/user.ts b/apps/builder/services/user.ts index 52e895cc6..451b3a21d 100644 --- a/apps/builder/services/user.ts +++ b/apps/builder/services/user.ts @@ -1,7 +1,9 @@ import { User } from 'db' -import { useSession } from 'next-auth/react' +import { sendRequest } from './utils' -export const useUser = (): User | undefined => { - const { data } = useSession() - return data?.user as User | undefined -} +export const updateUser = async (id: string, user: User) => + sendRequest({ + url: `/api/users/${id}`, + method: 'PUT', + body: user, + }) diff --git a/apps/builder/services/utils.ts b/apps/builder/services/utils.ts index 11d436fc9..d981d97d8 100644 --- a/apps/builder/services/utils.ts +++ b/apps/builder/services/utils.ts @@ -43,10 +43,6 @@ export const isDefined = (value: T | undefined | null): value is T => { return value !== undefined && value !== null } -export const isNotDefined = (value: T | undefined | null): value is T => { - return value === undefined || value === null -} - export const preventUserFromRefreshing = (e: BeforeUnloadEvent) => { e.preventDefault() e.returnValue = '' @@ -91,3 +87,26 @@ export const omit: Omit = (obj, ...keys) => { } return ret } + +export const uploadFile = async (file: File, key: string) => { + const res = await fetch( + `/api/storage/upload-url?key=${encodeURIComponent( + key + )}&fileType=${encodeURIComponent(file.type)}` + ) + const { url, fields } = await res.json() + const formData = new FormData() + + Object.entries({ ...fields, file }).forEach(([key, value]) => { + formData.append(key, value as string | Blob) + }) + + const upload = await fetch(url, { + method: 'POST', + body: formData, + }) + + return { + url: upload.ok ? `${url}/${key}` : null, + } +} diff --git a/packages/db/prisma/migrations/20211227145917_add_stripe_id/migration.sql b/packages/db/prisma/migrations/20211227145917_add_stripe_id/migration.sql new file mode 100644 index 000000000..dc15cb80f --- /dev/null +++ b/packages/db/prisma/migrations/20211227145917_add_stripe_id/migration.sql @@ -0,0 +1,22 @@ +/* + Warnings: + + - A unique constraint covering the columns `[stripeId]` on the table `User` will be added. If there are existing duplicate values, this will fail. + +*/ +-- AlterEnum +-- This migration adds more than one value to an enum. +-- With PostgreSQL versions 11 and earlier, this is not possible +-- in a single migration. This can be worked around by creating +-- multiple migrations, each migration adding only one value to +-- the enum. + + +ALTER TYPE "Plan" ADD VALUE 'LIFETIME'; +ALTER TYPE "Plan" ADD VALUE 'OFFERED'; + +-- AlterTable +ALTER TABLE "User" ADD COLUMN "stripeId" TEXT; + +-- CreateIndex +CREATE UNIQUE INDEX "User_stripeId_key" ON "User"("stripeId"); diff --git a/packages/db/prisma/schema.prisma b/packages/db/prisma/schema.prisma index daf9f2d1c..e44fd7b2d 100644 --- a/packages/db/prisma/schema.prisma +++ b/packages/db/prisma/schema.prisma @@ -48,11 +48,14 @@ model User { typebots Typebot[] folders DashboardFolder[] plan Plan @default(FREE) + stripeId String? @unique } enum Plan { FREE PRO + LIFETIME + OFFERED } model VerificationToken { diff --git a/yarn.lock b/yarn.lock index 3071bde70..761b5f042 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1286,6 +1286,11 @@ resolved "https://registry.yarnpkg.com/@rushstack/eslint-patch/-/eslint-patch-1.1.0.tgz#7f698254aadf921e48dda8c0a6b304026b8a9323" integrity sha512-JLo+Y592QzIE+q7Dl2pMUtt4q8SKYI5jDrZxrozEQxnGVOyYE+GWK9eLkwTaeN9DDctlaRAQ3TBmzZ1qdLE30A== +"@socket.io/component-emitter@~3.0.0": + version "3.0.0" + resolved "https://registry.yarnpkg.com/@socket.io/component-emitter/-/component-emitter-3.0.0.tgz#8863915676f837d9dad7b76f50cb500c1e9422e9" + integrity sha512-2pTGuibAXJswAPJjaKisthqS/NOK5ypG4LYT6tEAV0S/mxW0zOIvYvGK0V8w8+SHxAm6vRMSjqSalFXeBAqs+Q== + "@testing-library/cypress@^8.0.2": version "8.0.2" resolved "https://registry.yarnpkg.com/@testing-library/cypress/-/cypress-8.0.2.tgz#b13f0ff2424dec4368b6670dfbfb7e43af8eefc9" @@ -1359,6 +1364,13 @@ resolved "https://registry.yarnpkg.com/@types/aria-query/-/aria-query-4.2.2.tgz#ed4e0ad92306a704f9fb132a0cfcf77486dbe2bc" integrity sha512-HnYpAE1Y6kRyKM/XkEuiRQhTHvkzMBurTHnpFLYLBGPIylZNPs9jJcuOOYWxPLJCSEtmZT0Y8rHDokKN7rRTig== +"@types/engine.io@*": + version "3.1.7" + resolved "https://registry.yarnpkg.com/@types/engine.io/-/engine.io-3.1.7.tgz#86e541a5dc52fb7e97735383564a6ae4cfe2e8f5" + integrity sha512-qNjVXcrp+1sS8YpRUa714r0pgzOwESdW5UjHL7D/2ZFdBX0BXUXtg1LUrp+ylvqbvMcMWUy73YpRoxPN2VoKAQ== + dependencies: + "@types/node" "*" + "@types/estree@*": version "0.0.50" resolved "https://registry.yarnpkg.com/@types/estree/-/estree-0.0.50.tgz#1e0caa9364d3fccd2931c3ed96fdbeaa5d4cca83" @@ -1420,6 +1432,21 @@ resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.178.tgz#341f6d2247db528d4a13ddbb374bcdc80406f4f8" integrity sha512-0d5Wd09ItQWH1qFbEyQ7oTQ3GZrMfth5JkbN3EvTKLXcHLRDSXeLnlvlOn0wvxVIwK5o2M8JzP/OWz7T3NRsbw== +"@types/micro-cors@^0.1.2": + version "0.1.2" + resolved "https://registry.yarnpkg.com/@types/micro-cors/-/micro-cors-0.1.2.tgz#3004f5935d06b6f03016aa5975e8851e803d8374" + integrity sha512-dlNOHHaatKcuYXGMHkqICtFdthd+3tE8pFIrDTbDBYAtlTb7HzYh+NeEgfNWiARgOnsIOuoh2L2XR4UHow84nw== + dependencies: + "@types/micro" "*" + +"@types/micro@*": + version "7.3.6" + resolved "https://registry.yarnpkg.com/@types/micro/-/micro-7.3.6.tgz#7d68eb5a780ac4761e3b80687b4ee7328ebc3f2e" + integrity sha512-rZHvZ3+Ev3cGJJSy/wtSiXZmafU8guI07PHXf4ku9sQLfDuFALHMCiV+LuH4VOaeMMMnRs8nqxU392gxfn661g== + dependencies: + "@types/node" "*" + "@types/socket.io" "2.1.13" + "@types/mime-types@^2.1.0": version "2.1.1" resolved "https://registry.yarnpkg.com/@types/mime-types/-/mime-types-2.1.1.tgz#d9ba43490fa3a3df958759adf69396c3532cf2c1" @@ -1430,6 +1457,11 @@ resolved "https://registry.yarnpkg.com/@types/node/-/node-17.0.0.tgz#62797cee3b8b497f6547503b2312254d4fe3c2bb" integrity sha512-eMhwJXc931Ihh4tkU+Y7GiLzT/y/DBNpNtr4yU9O2w3SYBsr9NaOPhQlLKRmoWtI54uNwuo0IOUFQjVOTZYRvw== +"@types/node@>=8.1.0": + version "17.0.5" + resolved "https://registry.yarnpkg.com/@types/node/-/node-17.0.5.tgz#57ca67ec4e57ad9e4ef5a6bab48a15387a1c83e0" + integrity sha512-w3mrvNXLeDYV1GKTZorGJQivK6XLCoGwpnyJFbJVK/aTBQUxOCaa/GlFAAN3OTDFcb7h5tiFG+YXCO2By+riZw== + "@types/node@^14.14.31": version "14.18.0" resolved "https://registry.yarnpkg.com/@types/node/-/node-14.18.0.tgz#98df2397f6936bfbff4f089e40e06fa5dd88d32a" @@ -1521,6 +1553,22 @@ resolved "https://registry.yarnpkg.com/@types/sizzle/-/sizzle-2.3.3.tgz#ff5e2f1902969d305225a047c8a0fd5c915cebef" integrity sha512-JYM8x9EGF163bEyhdJBpR2QX1R5naCJHC8ucJylJ3w9/CVBaskdQ8WqBf8MmQrd1kRvp/a4TS8HJ+bxzR7ZJYQ== +"@types/socket.io-parser@*": + version "3.0.0" + resolved "https://registry.yarnpkg.com/@types/socket.io-parser/-/socket.io-parser-3.0.0.tgz#9726d3ab9235757a0a30dd5ccf8975dce54e5e2c" + integrity sha512-Ry/rbTE6HQNL9eu3LpL1Ocup5VexXu1bSSGlSho/IR5LuRc8YvxwSNJ3JxqTltVJEATLbZkMQETSbxfKNgp4Ew== + dependencies: + socket.io-parser "*" + +"@types/socket.io@2.1.13": + version "2.1.13" + resolved "https://registry.yarnpkg.com/@types/socket.io/-/socket.io-2.1.13.tgz#b6d694234e99956c96ff99e197eda824b6f9dc48" + integrity sha512-JRgH3nCgsWel4OPANkhH8TelpXvacAJ9VeryjuqCDiaVDMpLysd6sbt0dr6Z15pqH3p2YpOT3T1C5vQ+O/7uyg== + dependencies: + "@types/engine.io" "*" + "@types/node" "*" + "@types/socket.io-parser" "*" + "@types/testing-library__cypress@^5.0.9": version "5.0.9" resolved "https://registry.yarnpkg.com/@types/testing-library__cypress/-/testing-library__cypress-5.0.9.tgz#c65f2be0cbb7f11556c1a35fd767d8dd6d1dff23" @@ -2024,6 +2072,21 @@ available-typed-arrays@^1.0.5: resolved "https://registry.yarnpkg.com/available-typed-arrays/-/available-typed-arrays-1.0.5.tgz#92f95616501069d07d10edb2fc37d3e1c65123b7" integrity sha512-DMD0KiN46eipeziST1LPP/STfDU0sufISXmjSgvVsoU2tqxctQeASejWcfNtxYKqETM1UxQ8sp2OrSBWpHY6sw== +aws-sdk@^2.1048.0: + version "2.1048.0" + resolved "https://registry.yarnpkg.com/aws-sdk/-/aws-sdk-2.1048.0.tgz#02f2f35e0f51dd4510462e05c7a48fd4649d33f8" + integrity sha512-mVwWo+Udiuc/yEZ/DgJQGqOEtfiQjgUdtshx/t6ISe3+jW3TF9hUACwADwx2Sr/fuJyyJ3QD5JYLt5Cw35wQpA== + dependencies: + buffer "4.9.2" + events "1.1.1" + ieee754 "1.1.13" + jmespath "0.15.0" + querystring "0.2.0" + sax "1.2.1" + url "0.10.3" + uuid "3.3.2" + xml2js "0.4.19" + aws-sign2@~0.7.0: version "0.7.0" resolved "https://registry.yarnpkg.com/aws-sign2/-/aws-sign2-0.7.0.tgz#b46e890934a9591f2d2f6f86d7e6a9f1b3fe76a8" @@ -2238,6 +2301,15 @@ buffer-xor@^1.0.3: resolved "https://registry.yarnpkg.com/buffer-xor/-/buffer-xor-1.0.3.tgz#26e61ed1422fb70dd42e6e36729ed51d855fe8d9" integrity sha1-JuYe0UIvtw3ULm42cp7VHYVf6Nk= +buffer@4.9.2: + version "4.9.2" + resolved "https://registry.yarnpkg.com/buffer/-/buffer-4.9.2.tgz#230ead344002988644841ab0244af8c44bbe3ef8" + integrity sha512-xq+q3SRMOxGivLhBNaUdC64hDTQwejJ+H0T/NB1XMtTVEwNTrfFF3gAxiyW0Bu/xWEGhjVKgUcMhCrUy2+uCWg== + dependencies: + base64-js "^1.0.2" + ieee754 "^1.1.4" + isarray "^1.0.0" + buffer@5.6.0: version "5.6.0" resolved "https://registry.yarnpkg.com/buffer/-/buffer-5.6.0.tgz#a31749dc7d81d84db08abf937b6b8c4033f62786" @@ -2952,7 +3024,7 @@ debug@2, debug@^2.6.9: dependencies: ms "2.0.0" -debug@4, debug@^4.0.1, debug@^4.1.0, debug@^4.1.1, debug@^4.3.1, debug@^4.3.2: +debug@4, debug@^4.0.1, debug@^4.1.0, debug@^4.1.1, debug@^4.3.1, debug@^4.3.2, debug@~4.3.1: version "4.3.3" resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.3.tgz#04266e0b70a98d4462e6e288e38259213332b664" integrity sha512-/zxw5+vh1Tfv+4Qn7a5nsbcJKPaSvCDhojn6FEl9vupwK2VCSDtEiEtqr8DFtzYFOdz63LBkxec7DYuc2jon6Q== @@ -3574,6 +3646,11 @@ eventemitter3@^4.0.4: resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-4.0.7.tgz#2de9b68f6528d5644ef5c59526a1b4a07306169f" integrity sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw== +events@1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/events/-/events-1.1.1.tgz#9ebdb7635ad099c70dcc4c2a1f5004288e8bd924" + integrity sha1-nr23Y1rQmccNzEwqH1AEKI6L2SQ= + events@3.3.0: version "3.3.0" resolved "https://registry.yarnpkg.com/events/-/events-3.3.0.tgz#31a95ad0a924e2d2c419a813aeb2c4e878ea7400" @@ -4149,6 +4226,11 @@ icss-utils@^5.0.0: resolved "https://registry.yarnpkg.com/icss-utils/-/icss-utils-5.1.0.tgz#c6be6858abd013d768e98366ae47e25d5887b1ae" integrity sha512-soFhflCVWLfRNOPU3iv5Z9VUdT44xFRbzjLsEzSr5AQmgqPMTHdU3PMT1Cf1ssx8fLNJDA1juftYl+PUcv3MqA== +ieee754@1.1.13: + version "1.1.13" + resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.1.13.tgz#ec168558e95aa181fd87d37f55c32bbcb6708b84" + integrity sha512-4vf7I2LYV/HaWerSo3XmlMkp5eZ83i+/CDluXi/IGTs/O1sejBNhTtnxzmRZfvOUqj7lZjqHkeTvpgSFDlWZTg== + ieee754@^1.1.4: version "1.2.1" resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.2.1.tgz#8eb7a10a63fff25d15a57b001586d177d1b0d352" @@ -4459,7 +4541,7 @@ is-weakref@^1.0.1: dependencies: call-bind "^1.0.2" -isarray@~1.0.0: +isarray@^1.0.0, isarray@~1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11" integrity sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE= @@ -4497,6 +4579,11 @@ jest-worker@^26.2.1: merge-stream "^2.0.0" supports-color "^7.0.0" +jmespath@0.15.0: + version "0.15.0" + resolved "https://registry.yarnpkg.com/jmespath/-/jmespath-0.15.0.tgz#a3f222a9aae9f966f5d27c796510e28091764217" + integrity sha1-o/Iiqarp+Wb10nx5ZRDigJF2Qhc= + jose@^4.1.2, jose@^4.1.4: version "4.3.7" resolved "https://registry.yarnpkg.com/jose/-/jose-4.3.7.tgz#5000e4a2d41ae411a5abdd11e6baf63fc2973a69" @@ -4850,6 +4937,11 @@ merge2@^1.3.0: resolved "https://registry.yarnpkg.com/merge2/-/merge2-1.4.1.tgz#4368892f885e907455a6fd7dc55c0c9d404990ae" integrity sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg== +micro-cors@^0.1.1: + version "0.1.1" + resolved "https://registry.yarnpkg.com/micro-cors/-/micro-cors-0.1.1.tgz#af7a480182c114ffd1ada84ad9dffc52bb4f4054" + integrity sha512-6WqIahA5sbQR1Gjexp1VuWGFDKbZZleJb/gy1khNGk18a6iN1FdTcr3Q8twaxkV5H94RjxIBjirYbWCehpMBFw== + micromatch@^4.0.4: version "4.0.4" resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.4.tgz#896d519dfe9db25fce94ceb7a500919bf881ebf9" @@ -5963,6 +6055,13 @@ puppeteer@^2.1.1: rimraf "^2.6.1" ws "^6.1.0" +qs@^6.6.0: + version "6.10.2" + resolved "https://registry.yarnpkg.com/qs/-/qs-6.10.2.tgz#c1431bea37fc5b24c5bdbafa20f16bdf2a4b9ffe" + integrity sha512-mSIdjzqznWgfd4pMii7sHtaYF8rx8861hBO80SraY5GT0XQibWZWJSid0avzHGkDIZLImux2S5mXO0Hfct2QCw== + dependencies: + side-channel "^1.0.4" + qs@~6.5.2: version "6.5.2" resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.2.tgz#cb3ae806e8740444584ef154ce8ee98d403f3e36" @@ -6426,6 +6525,16 @@ safe-identifier@^0.4.2: resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== +sax@1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/sax/-/sax-1.2.1.tgz#7b8e656190b228e81a66aea748480d828cd2d37a" + integrity sha1-e45lYZCyKOgaZq6nSEgNgozS03o= + +sax@>=0.6.0: + version "1.2.4" + resolved "https://registry.yarnpkg.com/sax/-/sax-1.2.4.tgz#2816234e2378bddc4e5354fab5caa895df7100d9" + integrity sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw== + scheduler@^0.20.2: version "0.20.2" resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.20.2.tgz#4baee39436e34aa93b4874bddcbf0fe8b8b50e91" @@ -6609,6 +6718,14 @@ slice-ansi@^4.0.0: astral-regex "^2.0.0" is-fullwidth-code-point "^3.0.0" +socket.io-parser@*: + version "4.1.1" + resolved "https://registry.yarnpkg.com/socket.io-parser/-/socket.io-parser-4.1.1.tgz#0ad53d980781cab1eabe320417d8480c0133e62d" + integrity sha512-USQVLSkDWE5nbcY760ExdKaJxCE65kcsG/8k5FDGZVVxpD1pA7hABYXYkCUvxUuYYh/+uQw0N/fvBzfT8o07KA== + dependencies: + "@socket.io/component-emitter" "~3.0.0" + debug "~4.3.1" + source-map-js@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.0.1.tgz#a1741c131e3c77d048252adfa24e23b908670caf" @@ -6860,6 +6977,14 @@ strip-json-comments@^3.1.0, strip-json-comments@^3.1.1: resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-3.1.1.tgz#31f1281b3832630434831c310c01cccda8cbe006" integrity sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig== +stripe@^8.195.0: + version "8.195.0" + resolved "https://registry.yarnpkg.com/stripe/-/stripe-8.195.0.tgz#4d253e247aadb64d972488da9481fff743b58a11" + integrity sha512-pXEZFNJb4p9uZ69+B4A+zJEmBiFw3BzNG51ctPxUZij7ghFTnk2/RuUHmSGto2XVCcC46uG75czXVAvCUkOGtQ== + dependencies: + "@types/node" ">=8.1.0" + qs "^6.6.0" + style-inject@^0.3.0: version "0.3.0" resolved "https://registry.yarnpkg.com/style-inject/-/style-inject-0.3.0.tgz#d21c477affec91811cc82355832a700d22bf8dd3" @@ -7336,6 +7461,14 @@ uri-js@^4.2.2: dependencies: punycode "^2.1.0" +url@0.10.3: + version "0.10.3" + resolved "https://registry.yarnpkg.com/url/-/url-0.10.3.tgz#021e4d9c7705f21bbf37d03ceb58767402774c64" + integrity sha1-Ah5NnHcF8hu/N9A861h2dAJ3TGQ= + dependencies: + punycode "1.3.2" + querystring "0.2.0" + url@^0.11.0: version "0.11.0" resolved "https://registry.yarnpkg.com/url/-/url-0.11.0.tgz#3838e97cfc60521eb73c525a8e55bfdd9e2e28f1" @@ -7393,6 +7526,11 @@ util@0.12.4, util@^0.12.0: safe-buffer "^5.1.2" which-typed-array "^1.1.2" +uuid@3.3.2: + version "3.3.2" + resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.3.2.tgz#1b4af4955eb3077c501c23872fc6513811587131" + integrity sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA== + uuid@^8.3.2: version "8.3.2" resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.2.tgz#80d5b5ced271bb9af6c445f21a1a04c606cefbe2" @@ -7526,6 +7664,19 @@ ws@^6.1.0: dependencies: async-limiter "~1.0.0" +xml2js@0.4.19: + version "0.4.19" + resolved "https://registry.yarnpkg.com/xml2js/-/xml2js-0.4.19.tgz#686c20f213209e94abf0d1bcf1efaa291c7827a7" + integrity sha512-esZnJZJOiJR9wWKMyuvSE1y6Dq5LCuJanqhxslH2bxM6duahNZ+HMpCLhBQGZkbX6xRf8x1Y2eJlgt2q3qo49Q== + dependencies: + sax ">=0.6.0" + xmlbuilder "~9.0.1" + +xmlbuilder@~9.0.1: + version "9.0.7" + resolved "https://registry.yarnpkg.com/xmlbuilder/-/xmlbuilder-9.0.7.tgz#132ee63d2ec5565c557e20f4c22df9aca686b10d" + integrity sha1-Ey7mPS7FVlxVfiD0wi35rKaGsQ0= + xtend@^4.0.2: version "4.0.2" resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.2.tgz#bb72779f5fa465186b1f438f674fa347fdb5db54"