diff --git a/apps/web/package.json b/apps/web/package.json index 9a04b9421..2405befd8 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -48,6 +48,7 @@ "iron-session": "^6.3.1", "js-cookie": "^3.0.1", "lodash": "^4.17.21", + "micro": "^10.0.1", "nanoid": "^4.0.0", "next-i18next": "^13.0.3", "next-seo": "^5.15.0", diff --git a/apps/web/public/locales/en/app.json b/apps/web/public/locales/en/app.json index f3a2c8e07..3c50e26e4 100644 --- a/apps/web/public/locales/en/app.json +++ b/apps/web/public/locales/en/app.json @@ -211,8 +211,6 @@ "duplicateDescription": "Create a new poll based on this one", "duplicateTitleLabel": "Title", "duplicateTitleDescription": "Hint: Give your new poll a unique title", - "thankYou": "Thank you!", - "pleaseWait": "Your account is being upgraded. This should only take a few seconds.", "proFeature": "Pro Feature", "upgradeOverlaySubtitle2": "Please upgrade to a paid plan to use this feature. This is how we keep the lights on :)", "savePercent": "Save {percent}%", @@ -227,5 +225,10 @@ "scrollLeft": "Scroll Left", "scrollRight": "Scroll Right", "shrink": "Shrink", - "expand": "Expand" + "expand": "Expand", + "activeSubscription": "Thank you for subscribing to Rallly Pro. You can manage your subscription and billing details from the billing portal.", + "billingPortal": "Billing Portal", + "supportDescription": "Need help with anything?", + "supportBilling": "Please reach out if you need any assistance.", + "contactSupport": "Contact Support" } diff --git a/apps/web/src/components/pay-wall.tsx b/apps/web/src/components/pay-wall.tsx index 4df76b8c5..78b7ede11 100644 --- a/apps/web/src/components/pay-wall.tsx +++ b/apps/web/src/components/pay-wall.tsx @@ -1,11 +1,9 @@ -import { trpc } from "@rallly/backend"; import { CalendarCheck2Icon, CopyIcon, DatabaseIcon, HeartIcon, ImageOffIcon, - Loader2Icon, LockIcon, Settings2Icon, TrendingUpIcon, @@ -51,32 +49,8 @@ const Feature = ({ ); }; -const ThankYou = () => { - trpc.user.getBilling.useQuery(undefined, { - refetchInterval: 1000, - }); - - return ( -
-

- -

-

- -

-
- -
-
- ); -}; - const Teaser = () => { const router = useRouter(); - const [didUpgrade, setDidUpgrade] = React.useState(false); const [tab, setTab] = React.useState("yearly"); @@ -106,140 +80,132 @@ const Teaser = () => { className="text-center" aria-hidden="true" > - + - {didUpgrade ? ( - - ) : ( -
-
-

- -

-

- -

-
- - - - - - - - - - -
-
-
$5
-
-
USD
-
-
-
- -
-
-
- -
-
-
-
- $5 -
-
$2.50
-
-
-
USD
-
-
-
- -
-

- - - -

-
-
-
-
-

- - -

-

- - -

-
-

+ +
+
+

+ +

+

-

-
    - - - - - - - - - - - - - - - - - - -
-
- setDidUpgrade(true)} - > - - - -
+

- )} + + + + + + + + + + +
+
+
$5
+
+
USD
+
+
+
+ +
+
+
+ +
+
+
+
$5
+
$2.50
+
+
+
USD
+
+
+
+ +
+

+ + + +

+
+
+
+
+

+ + +

+

+ + +

+
+

+ +

+ +
+ + + + +
+ ); }; diff --git a/apps/web/src/components/upgrade-button.tsx b/apps/web/src/components/upgrade-button.tsx index 9fd9f4827..5ac5cc4f1 100644 --- a/apps/web/src/components/upgrade-button.tsx +++ b/apps/web/src/components/upgrade-button.tsx @@ -1,56 +1,32 @@ -import { trpc } from "@rallly/backend"; import { Button } from "@rallly/ui/button"; -import { useRouter } from "next/router"; +import Link from "next/link"; import React from "react"; -import { useUser } from "@/components/user-provider"; -import { planIdMonthly, planIdYearly } from "@/utils/constants"; import { usePostHog } from "@/utils/posthog"; export const UpgradeButton = ({ children, - onUpgrade, annual, -}: React.PropsWithChildren<{ annual?: boolean; onUpgrade?: () => void }>) => { +}: React.PropsWithChildren<{ annual?: boolean }>) => { const posthog = usePostHog(); - const [isPendingSubscription, setPendingSubscription] = React.useState(false); - const { user } = useUser(); - const router = useRouter(); - - trpc.user.getBilling.useQuery(undefined, { - refetchInterval: isPendingSubscription ? 1000 : 0, - }); return ( <> ); diff --git a/apps/web/src/components/user-dropdown.tsx b/apps/web/src/components/user-dropdown.tsx index ff441f8dd..46a44a9d4 100644 --- a/apps/web/src/components/user-dropdown.tsx +++ b/apps/web/src/components/user-dropdown.tsx @@ -1,4 +1,3 @@ -import { trpc } from "@rallly/backend"; import { ChevronDown, CreditCardIcon, @@ -26,19 +25,15 @@ import Link from "next/link"; import { Trans } from "@/components/trans"; import { CurrentUserAvatar } from "@/components/user"; +import { usePlan } from "@/contexts/plan"; import { isFeedbackEnabled } from "@/utils/constants"; import { IfAuthenticated, IfGuest, useUser } from "./user-provider"; const Plan = () => { - const { isFetched, data } = trpc.user.getBilling.useQuery(); - if (!isFetched) { - return null; - } + const plan = usePlan(); - const isPlus = data && data.endDate.getTime() > Date.now(); - - if (isPlus) { + if (plan === "paid") { return ( diff --git a/apps/web/src/components/user-provider.tsx b/apps/web/src/components/user-provider.tsx index f7dd7b558..dd7bc8d64 100644 --- a/apps/web/src/components/user-provider.tsx +++ b/apps/web/src/components/user-provider.tsx @@ -50,7 +50,7 @@ export const UserProvider = (props: { children?: React.ReactNode }) => { const queryClient = trpc.useContext(); const user = useWhoAmI(); - const billingQuery = trpc.user.getBilling.useQuery(); + const subscriptionQuery = trpc.user.subscription.useQuery(); const { data: userPreferences } = trpc.userPreferences.get.useQuery(); const shortName = user @@ -59,7 +59,7 @@ export const UserProvider = (props: { children?: React.ReactNode }) => { : user.id.substring(0, 10) : t("guest"); - if (!user || userPreferences === undefined || !billingQuery.isFetched) { + if (!user || userPreferences === undefined || !subscriptionQuery.isFetched) { return null; } diff --git a/apps/web/src/contexts/plan.tsx b/apps/web/src/contexts/plan.tsx index 4258521ca..9687cfde2 100644 --- a/apps/web/src/contexts/plan.tsx +++ b/apps/web/src/contexts/plan.tsx @@ -1,11 +1,9 @@ import { trpc } from "@rallly/backend"; export const usePlan = () => { - const { data } = trpc.user.getBilling.useQuery(undefined, { - staleTime: 10 * 1000, - }); + const { data } = trpc.user.subscription.useQuery(); - const isPaid = Boolean(data && data.endDate.getTime() > Date.now()); + const isPaid = data?.active === true; return isPaid ? "paid" : "free"; }; diff --git a/apps/web/src/pages/api/stripe/checkout.ts b/apps/web/src/pages/api/stripe/checkout.ts new file mode 100644 index 000000000..a453a17c2 --- /dev/null +++ b/apps/web/src/pages/api/stripe/checkout.ts @@ -0,0 +1,105 @@ +import { getSession } from "@rallly/backend/next/session"; +import { stripe } from "@rallly/backend/stripe"; +import { prisma } from "@rallly/database"; +import { absoluteUrl } from "@rallly/utils"; +import { NextApiRequest, NextApiResponse } from "next"; +import { z } from "zod"; + +export const config = { + edge: true, +}; + +const inputSchema = z.object({ + period: z.enum(["monthly", "yearly"]).optional(), + success_path: z.string().optional(), + return_path: z.string().optional(), +}); + +export default async function handler( + req: NextApiRequest, + res: NextApiResponse, +) { + const userSession = await getSession(req, res); + + if (userSession.user?.isGuest !== false) { + // You need to be logged in to subscribe + return res + .status(403) + .redirect( + `/login${req.url ? `?redirect=${encodeURIComponent(req.url)}` : ""}`, + ); + } + + const { period = "monthly", return_path } = inputSchema.parse(req.query); + + const user = await prisma.user.findUnique({ + where: { + id: userSession.user.id, + }, + select: { + email: true, + customerId: true, + subscription: { + select: { + active: true, + }, + }, + }, + }); + + if (!user) { + return res.status(403).redirect("/logout"); + } + + if (user.subscription?.active === true) { + // User already has an active subscription. Take them to customer portal + return res.redirect("/api/stripe/portal"); + } + + const session = await stripe.checkout.sessions.create({ + success_url: absoluteUrl( + return_path ?? "/api/stripe/portal?session_id={CHECKOUT_SESSION_ID}", + ), + cancel_url: absoluteUrl(return_path), + ...(user.customerId + ? { + // use existing customer if available to reuse payment details + customer: user.customerId, + customer_update: { + // needed for tax id collection + name: "auto", + }, + } + : { + // supply email if user is not a customer yet + customer_email: user.email, + }), + mode: "subscription", + allow_promotion_codes: true, + billing_address_collection: "auto", + tax_id_collection: { + enabled: true, + }, + metadata: { + userId: userSession.user.id, + }, + line_items: [ + { + price: + period === "yearly" + ? (process.env.STRIPE_YEARLY_PRICE as string) + : (process.env.STRIPE_MONTHLY_PRICE as string), + quantity: 1, + }, + ], + }); + + if (session.url) { + // redirect to checkout session + return res.status(303).redirect(session.url); + } + + return res + .status(500) + .json({ error: "Something went wrong while creating a checkout session" }); +} diff --git a/apps/web/src/pages/api/stripe/portal.ts b/apps/web/src/pages/api/stripe/portal.ts new file mode 100644 index 000000000..0e184fe9d --- /dev/null +++ b/apps/web/src/pages/api/stripe/portal.ts @@ -0,0 +1,58 @@ +import { getSession } from "@rallly/backend/next/session"; +import { stripe } from "@rallly/backend/stripe"; +import { absoluteUrl } from "@rallly/utils"; +import { NextApiRequest, NextApiResponse } from "next"; +import { z } from "zod"; + +const inputSchema = z.object({ + session_id: z.string().optional(), + return_path: z.string().optional(), +}); + +export default async function handler( + req: NextApiRequest, + res: NextApiResponse, +) { + const userSession = await getSession(req, res); + + if (userSession.user?.isGuest !== false) { + // You need to be logged in to subscribe + return res + .status(403) + .redirect( + `/login${req.url ? `?redirect=${encodeURIComponent(req.url)}` : ""}`, + ); + } + + const user = await prisma?.user.findUnique({ + where: { + id: userSession.user.id, + }, + select: { + email: true, + customerId: true, + }, + }); + + if (!user) { + return res.status(403).redirect("/logout"); + } + + const { session_id: sessionId, return_path } = inputSchema.parse(req.query); + + let customerId: string; + + if (sessionId) { + const session = await stripe.checkout.sessions.retrieve(sessionId); + customerId = session.customer as string; + } else { + customerId = user.customerId as string; + } + + const portalSession = await stripe.billingPortal.sessions.create({ + customer: customerId, + return_url: absoluteUrl(return_path), + }); + + res.status(303).redirect(portalSession.url); +} diff --git a/apps/web/src/pages/api/stripe/webhook.ts b/apps/web/src/pages/api/stripe/webhook.ts new file mode 100644 index 000000000..d955bc0d3 --- /dev/null +++ b/apps/web/src/pages/api/stripe/webhook.ts @@ -0,0 +1,110 @@ +import type { Stripe } from "@rallly/backend/stripe"; +import { stripe } from "@rallly/backend/stripe"; +import { prisma } from "@rallly/database"; +import { buffer } from "micro"; +import { NextApiRequest, NextApiResponse } from "next"; +import { z } from "zod"; + +export const config = { + api: { + bodyParser: false, + }, +}; + +const toDate = (date: number) => new Date(date * 1000); + +const endpointSecret = process.env.STRIPE_SIGNING_SECRET as string; + +const validatedWebhook = async (req: NextApiRequest) => { + const signature = req.headers["stripe-signature"] as string; + const buf = await buffer(req); + + try { + return stripe.webhooks.constructEvent(buf, signature, endpointSecret); + } catch (err) { + return null; + } +}; + +const metadataSchema = z.object({ + userId: z.string(), +}); + +export default async function handler( + req: NextApiRequest, + res: NextApiResponse, +) { + if (req.method !== "POST") { + return res.status(405).end(); + } + if (!endpointSecret) { + return res.status(400).send("No endpoint secret"); + } + const event = await validatedWebhook(req); + + if (!event) { + return res.status(400).send("Invalid signature"); + } + + switch (event.type) { + case "checkout.session.completed": + const checkoutSession = event.data.object as Stripe.Checkout.Session; + const { userId } = metadataSchema.parse(checkoutSession.metadata); + + if (!userId) { + return res.status(400).send("Missing client reference ID"); + } + + await prisma.user.update({ + where: { + id: userId, + }, + data: { + customerId: checkoutSession.customer as string, + subscriptionId: checkoutSession.subscription as string, + }, + }); + break; + case "customer.subscription.deleted": + case "customer.subscription.updated": + case "customer.subscription.created": + const subscription = event.data.object as Stripe.Subscription; + + // check if the subscription is active + const isActive = subscription.status === "active"; + + // get the subscription price details + const lineItem = subscription.items.data[0]; + + // update/create the subscription in the database + const { price } = lineItem; + await prisma.subscription.upsert({ + where: { + id: subscription.id, + }, + create: { + id: subscription.id, + active: isActive, + priceId: price.id, + currency: price.currency ?? null, + createdAt: toDate(subscription.created), + periodStart: toDate(subscription.current_period_start), + periodEnd: toDate(subscription.current_period_end), + }, + update: { + active: isActive, + priceId: price.id, + currency: price.currency ?? null, + createdAt: toDate(subscription.created), + periodStart: toDate(subscription.current_period_start), + periodEnd: toDate(subscription.current_period_end), + }, + }); + break; + default: + // Unexpected event type + console.error(`Unhandled event type ${event.type}.`); + } + + res.end(); +} diff --git a/apps/web/src/pages/settings/billing.tsx b/apps/web/src/pages/settings/billing.tsx index 9eb7dac18..43b8a9aaa 100644 --- a/apps/web/src/pages/settings/billing.tsx +++ b/apps/web/src/pages/settings/billing.tsx @@ -1,5 +1,6 @@ import { trpc } from "@rallly/backend"; -import { CreditCardIcon } from "@rallly/icons"; +import { ArrowUpRight, CreditCardIcon } from "@rallly/icons"; +import { Badge } from "@rallly/ui/badge"; import { Button } from "@rallly/ui/button"; import { Card } from "@rallly/ui/card"; import { Label } from "@rallly/ui/label"; @@ -13,7 +14,6 @@ import { getProfileLayout } from "@/components/layouts/profile-layout"; import { SettingsSection } from "@/components/settings/settings-section"; import { Trans } from "@/components/trans"; import { useUser } from "@/components/user-provider"; -import { usePlan } from "@/contexts/plan"; import { NextPageWithLayout } from "../../types"; import { getStaticTranslations } from "../../utils/with-page-translations"; @@ -25,6 +25,38 @@ declare global { } } +const BillingPortal = () => { + return ( +
+
+ + + +
+

+ +

+
+ +
+
+ ); +}; + export const proPlanIdMonthly = process.env .NEXT_PUBLIC_PRO_PLAN_ID_MONTHLY as string; @@ -34,25 +66,31 @@ export const proPlanIdYearly = process.env const SubscriptionStatus = () => { const { user } = useUser(); - const plan = usePlan(); - const isPlus = plan === "paid"; + const { data } = trpc.user.subscription.useQuery(); + + if (!data) { + return null; + } if (user.isGuest) { return <>You need to be logged in.; } - if (isPlus) { - return ; + if (data.legacy) { + // User is on the old billing system + return ; + } else if (data.active) { + return ; } else { return ; } }; -const BillingStatus = () => { +const LegacyBilling = () => { const { data: userPaymentData } = trpc.user.getBilling.useQuery(); if (!userPaymentData) { - return

Something when wrong. Missing user payment data.

; + return null; } const { status, endDate, planId } = userPaymentData; @@ -181,6 +219,29 @@ const Page: NextPageWithLayout = () => { > + } + description={ + + } + > +
+

+ +

+ +
+
); }; diff --git a/packages/backend/next/edge.ts b/packages/backend/next/edge.ts index c1d41fe83..57397898e 100644 --- a/packages/backend/next/edge.ts +++ b/packages/backend/next/edge.ts @@ -1,8 +1,11 @@ +import type { IncomingMessage, ServerResponse } from "http"; import { getIronSession } from "iron-session/edge"; -import { NextRequest, NextResponse } from "next/server"; import { sessionConfig } from "../session-config"; -export const getSession = async (req: NextRequest, res: NextResponse) => { +export const getSession = async ( + req: Request | IncomingMessage, + res: Response | ServerResponse, +) => { return getIronSession(req, res, sessionConfig); }; diff --git a/packages/backend/next/session.ts b/packages/backend/next/session.ts index 5ebd76d0c..7c9037649 100644 --- a/packages/backend/next/session.ts +++ b/packages/backend/next/session.ts @@ -1,3 +1,5 @@ +import type { IncomingMessage, ServerResponse } from "http"; +import { getIronSession } from "iron-session"; import { withIronSessionApiRoute, withIronSessionSsr } from "iron-session/next"; import { GetServerSideProps, @@ -13,6 +15,13 @@ export function withSessionRoute(handler: NextApiHandler) { return withIronSessionApiRoute(handler, sessionConfig); } +export const getSession = async ( + req: Request | IncomingMessage, + res: Response | ServerResponse, +) => { + return getIronSession(req, res, sessionConfig); +}; + export function withSessionSsr( handler: GetServerSideProps | GetServerSideProps[], options?: { diff --git a/packages/backend/package.json b/packages/backend/package.json index 6e9a6373f..cefb62472 100644 --- a/packages/backend/package.json +++ b/packages/backend/package.json @@ -19,6 +19,7 @@ "@trpc/server": "^10.13.0", "iron-session": "^6.3.1", "spacetime": "^7.4.4", + "stripe": "^13.2.0", "superjson": "^1.12.2", "timezone-soft": "^1.4.1" } diff --git a/packages/backend/stripe.ts b/packages/backend/stripe.ts new file mode 100644 index 000000000..cb669ca53 --- /dev/null +++ b/packages/backend/stripe.ts @@ -0,0 +1,8 @@ +import Stripe from "stripe"; + +export type { Stripe } from "stripe"; + +export const stripe = new Stripe(process.env.STRIPE_SECRET_KEY as string, { + apiVersion: "2023-08-16", + typescript: true, +}); diff --git a/packages/backend/trpc/routers/user.ts b/packages/backend/trpc/routers/user.ts index 400d69f6a..e29a93677 100644 --- a/packages/backend/trpc/routers/user.ts +++ b/packages/backend/trpc/routers/user.ts @@ -1,10 +1,10 @@ import { prisma } from "@rallly/database"; import { z } from "zod"; -import { publicProcedure, router } from "../trpc"; +import { privateProcedure, router } from "../trpc"; export const user = router({ - getBilling: publicProcedure.query(async ({ ctx }) => { + getBilling: privateProcedure.query(async ({ ctx }) => { return await prisma.userPaymentData.findUnique({ select: { subscriptionId: true, @@ -19,7 +19,50 @@ export const user = router({ }, }); }), - changeName: publicProcedure + subscription: privateProcedure.query(async ({ ctx }) => { + const user = await prisma.user.findUnique({ + where: { + id: ctx.user.id, + }, + select: { + subscription: { + select: { + active: true, + }, + }, + }, + }); + + if (user?.subscription?.active === true) { + return { + active: true, + }; + } + + const userPaymentData = await prisma.userPaymentData.findUnique({ + where: { + userId: ctx.user.id, + }, + select: { + endDate: true, + }, + }); + + if ( + userPaymentData?.endDate && + userPaymentData.endDate.getTime() > Date.now() + ) { + return { + active: true, + legacy: true, + }; + } + + return { + active: false, + }; + }), + changeName: privateProcedure .input( z.object({ name: z.string().min(1).max(100), diff --git a/packages/database/prisma/migrations/20230823084154_stripe_subscriptions/migration.sql b/packages/database/prisma/migrations/20230823084154_stripe_subscriptions/migration.sql new file mode 100644 index 000000000..872cb5d00 --- /dev/null +++ b/packages/database/prisma/migrations/20230823084154_stripe_subscriptions/migration.sql @@ -0,0 +1,27 @@ +/* + Warnings: + + - A unique constraint covering the columns `[subscription_id]` on the table `users` will be added. If there are existing duplicate values, this will fail. + +*/ +-- AlterTable +ALTER TABLE "users" ADD COLUMN "customer_id" TEXT, +ADD COLUMN "subscription_id" TEXT; + +-- CreateTable +CREATE TABLE "subscriptions" ( + "id" TEXT NOT NULL, + "price_id" TEXT NOT NULL, + "active" BOOLEAN NOT NULL, + "currency" TEXT, + "interval" TEXT, + "interval_count" INTEGER, + "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "period_start" TIMESTAMP(3) NOT NULL, + "period_end" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "subscriptions_pkey" PRIMARY KEY ("id") +); + +-- CreateIndex +CREATE UNIQUE INDEX "users_subscription_id_key" ON "users"("subscription_id"); diff --git a/packages/database/prisma/schema.prisma b/packages/database/prisma/schema.prisma index 737699512..7c3671534 100644 --- a/packages/database/prisma/schema.prisma +++ b/packages/database/prisma/schema.prisma @@ -17,15 +17,18 @@ enum TimeFormat { } model User { - id String @id @default(cuid()) - name String - email String @unique() @db.Citext - createdAt DateTime @default(now()) @map("created_at") - updatedAt DateTime? @updatedAt @map("updated_at") - comments Comment[] - polls Poll[] - watcher Watcher[] - events Event[] + id String @id @default(cuid()) + name String + email String @unique() @db.Citext + createdAt DateTime @default(now()) @map("created_at") + updatedAt DateTime? @updatedAt @map("updated_at") + comments Comment[] + polls Poll[] + watcher Watcher[] + events Event[] + customerId String? @map("customer_id") + subscription Subscription? @relation(fields: [subscriptionId], references: [id]) + subscriptionId String? @unique @map("subscription_id") @@map("users") } @@ -52,6 +55,21 @@ model UserPaymentData { @@map("user_payment_data") } +model Subscription { + id String @id + priceId String @map("price_id") + active Boolean + currency String? + interval String? + intervalCount Int? @map("interval_count") + createdAt DateTime @default(now()) @map("created_at") + periodStart DateTime @map("period_start") + periodEnd DateTime @map("period_end") + user User? + + @@map("subscriptions") +} + model UserPreferences { userId String @id @map("user_id") timeZone String? @map("time_zone") diff --git a/turbo.json b/turbo.json index c95cec1a2..23b445ef0 100644 --- a/turbo.json +++ b/turbo.json @@ -59,7 +59,6 @@ "EMAIL_PROVIDER", "MAINTENANCE_MODE", "NEXT_PUBLIC_APP_BASE_URL", - "NEXT_PUBLIC_SHORT_BASE_URL", "NEXT_PUBLIC_APP_VERSION", "NEXT_PUBLIC_BASE_URL", "NEXT_PUBLIC_BETA", @@ -76,6 +75,7 @@ "NEXT_PUBLIC_PRO_PLAN_ID_MONTHLY", "NEXT_PUBLIC_PRO_PLAN_ID_YEARLY", "NEXT_PUBLIC_SENTRY_DSN", + "NEXT_PUBLIC_SHORT_BASE_URL", "NEXT_PUBLIC_VERCEL_URL", "NODE_ENV", "NOREPLY_EMAIL", @@ -90,6 +90,10 @@ "SMTP_SECURE", "SMTP_TLS_ENABLED", "SMTP_USER", + "STRIPE_MONTHLY_PRICE", + "STRIPE_SECRET_KEY", + "STRIPE_SIGNING_SECRET", + "STRIPE_YEARLY_PRICE", "SUPPORT_EMAIL" ] } diff --git a/yarn.lock b/yarn.lock index 7098e81b4..6be446f05 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3839,6 +3839,11 @@ resolved "https://registry.npmjs.org/@types/node/-/node-18.14.2.tgz" integrity sha512-1uEQxww3DaghA0RxqHx0O0ppVlo43pJhepY51OxuQIKHpjbnYLA7vcdwioNPzIqmC2u3I/dmylcqjlh0e7AyUA== +"@types/node@>=8.1.0": + version "20.5.0" + resolved "https://registry.yarnpkg.com/@types/node/-/node-20.5.0.tgz#7fc8636d5f1aaa3b21e6245e97d56b7f56702313" + integrity sha512-Mgq7eCtoTjT89FqNoTzzXg2XvCi5VMhRV6+I2aYanc6kQCBImeNaAYRs/DyoVqk1YEUJK5gN9VO7HRIdz4Wo3Q== + "@types/node@^12.7.1": version "12.20.55" resolved "https://registry.npmjs.org/@types/node/-/node-12.20.55.tgz" @@ -4216,6 +4221,11 @@ append-buffer@^1.0.2: dependencies: buffer-equal "^1.0.0" +arg@4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/arg/-/arg-4.1.0.tgz#583c518199419e0037abb74062c37f8519e575f0" + integrity sha512-ZWc51jO3qegGkVh8Hwpv636EkbesNV5ZNQPCtRa+0qytRYPEs9IYT9qITY9buezqUH5uqyzlWLcufrzU2rffdg== + arg@^4.1.0: version "4.1.3" resolved "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz" @@ -4548,6 +4558,11 @@ buffers@~0.1.1: resolved "https://registry.npmjs.org/buffers/-/buffers-0.1.1.tgz" integrity sha512-9q/rDEGSb/Qsvv2qvzIzdluL5k7AaJOTrw23z9reQthrbF7is4CtlT0DXyO1oei2DCp4uojjzQ7igaSHp1kAEQ== +bytes@3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.1.0.tgz#f6cf7933a360e0588fa9fde85651cdc7f805d1f6" + integrity sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg== + call-bind@^1.0.0, call-bind@^1.0.2: version "1.0.2" resolved "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz" @@ -4857,6 +4872,11 @@ config-chain@^1.1.13: ini "^1.3.4" proto-list "~1.2.1" +content-type@1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.4.tgz#e138cc75e040c727b1966fe5e5f8c9aee256fe3b" + integrity sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA== + convert-source-map@^1.5.0, convert-source-map@^1.7.0: version "1.9.0" resolved "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz" @@ -5122,6 +5142,11 @@ define-properties@^1.1.3, define-properties@^1.1.4: has-property-descriptors "^1.0.0" object-keys "^1.1.1" +depd@~1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/depd/-/depd-1.1.2.tgz#9bcd52e14c097763e749b274c4346ed2e560b5a9" + integrity sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ== + deprecation@^2.0.0, deprecation@^2.3.1: version "2.3.1" resolved "https://registry.npmjs.org/deprecation/-/deprecation-2.3.1.tgz" @@ -6489,6 +6514,17 @@ htmlparser2@^8.0.1: domutils "^3.0.1" entities "^4.3.0" +http-errors@1.7.3: + version "1.7.3" + resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.7.3.tgz#6c619e4f9c60308c38519498c14fbb10aacebb06" + integrity sha512-ZTTX0MWrsQ2ZAhA1cejAwDLycFsd7I7nVtnkT3Ol0aqodaKW+0CTZDQ1uBv5whptCnc8e8HeRRJxRs0kmm/Qfw== + dependencies: + depd "~1.1.2" + inherits "2.0.4" + setprototypeof "1.1.1" + statuses ">= 1.5.0 < 2" + toidentifier "1.0.0" + https-proxy-agent@^5.0.0: version "5.0.1" resolved "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz" @@ -6564,6 +6600,13 @@ i18next@^22.4.9: dependencies: "@babel/runtime" "^7.20.6" +iconv-lite@0.4.24: + version "0.4.24" + resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b" + integrity sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA== + dependencies: + safer-buffer ">= 2.1.2 < 3" + iconv-lite@0.6.3: version "0.6.3" resolved "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz" @@ -6620,7 +6663,7 @@ inflight@^1.0.4: once "^1.3.0" wrappy "1" -inherits@2, inherits@^2.0.1, inherits@^2.0.3, inherits@^2.0.4, inherits@~2.0.0, inherits@~2.0.3: +inherits@2, inherits@2.0.4, inherits@^2.0.1, inherits@^2.0.3, inherits@^2.0.4, inherits@~2.0.0, inherits@~2.0.3: version "2.0.4" resolved "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz" integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== @@ -7540,6 +7583,15 @@ merge2@^1.3.0, merge2@^1.4.1: resolved "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz" integrity sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg== +micro@^10.0.1: + version "10.0.1" + resolved "https://registry.yarnpkg.com/micro/-/micro-10.0.1.tgz#2601e02b0dacd2eaee77e9de18f12b2e595c5951" + integrity sha512-9uwZSsUrqf6+4FLLpiPj5TRWQv5w5uJrJwsx1LR/TjqvQmKC1XnGQ9OHrFwR3cbZ46YqPqxO/XJCOpWnqMPw2Q== + dependencies: + arg "4.1.0" + content-type "1.0.4" + raw-body "2.4.1" + micromark-core-commonmark@^1.0.1: version "1.1.0" resolved "https://registry.yarnpkg.com/micromark-core-commonmark/-/micromark-core-commonmark-1.1.0.tgz#1386628df59946b2d39fb2edfd10f3e8e0a75bb8" @@ -8509,11 +8561,28 @@ pvutils@^1.1.3: resolved "https://registry.npmjs.org/pvutils/-/pvutils-1.1.3.tgz" integrity sha512-pMpnA0qRdFp32b1sJl1wOJNxZLQ2cbQx+k6tjNtZ8CpvVhNqEPRgivZ2WOUev2YMajecdH7ctUPDvEe87nariQ== +qs@^6.11.0: + version "6.11.2" + resolved "https://registry.yarnpkg.com/qs/-/qs-6.11.2.tgz#64bea51f12c1f5da1bc01496f48ffcff7c69d7d9" + integrity sha512-tDNIz22aBzCDxLtVH++VnTfzxlfeK5CbqohpSqpJgj1Wg/cQbStNAz3NuqCs5vV+pjBsK4x4pN9HlVh7rcYRiA== + dependencies: + side-channel "^1.0.4" + queue-microtask@^1.2.2: version "1.2.3" resolved "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz" integrity sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A== +raw-body@2.4.1: + version "2.4.1" + resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.4.1.tgz#30ac82f98bb5ae8c152e67149dac8d55153b168c" + integrity sha512-9WmIKF6mkvA0SLmA2Knm9+qj89e+j1zqgyn8aXGd7+nAduPoqgI9lO57SAZNn/Byzo5P7JhXTyg9PzaJbH73bA== + dependencies: + bytes "3.1.0" + http-errors "1.7.3" + iconv-lite "0.4.24" + unpipe "1.0.0" + react-big-calendar@^1.8.1: version "1.8.1" resolved "https://registry.yarnpkg.com/react-big-calendar/-/react-big-calendar-1.8.1.tgz#07886a66086fcae16934572c5ace8c4c433dbbed" @@ -9041,7 +9110,7 @@ safe-regex-test@^1.0.0: get-intrinsic "^1.1.3" is-regex "^1.1.4" -"safer-buffer@>= 2.1.2 < 3.0.0": +"safer-buffer@>= 2.1.2 < 3", "safer-buffer@>= 2.1.2 < 3.0.0": version "2.1.2" resolved "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz" integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== @@ -9116,6 +9185,11 @@ setimmediate@~1.0.4: resolved "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz" integrity sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA== +setprototypeof@1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.1.1.tgz#7e95acb24aa92f5885e0abef5ba131330d4ae683" + integrity sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw== + shallow-clone@^3.0.0: version "3.0.1" resolved "https://registry.npmjs.org/shallow-clone/-/shallow-clone-3.0.1.tgz" @@ -9325,6 +9399,11 @@ stacktrace-parser@^0.1.10: dependencies: type-fest "^0.7.1" +"statuses@>= 1.5.0 < 2": + version "1.5.0" + resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.5.0.tgz#161c7dac177659fd9811f43771fa99381478628c" + integrity sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA== + stop-iteration-iterator@^1.0.0: version "1.0.0" resolved "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.0.0.tgz" @@ -9432,6 +9511,14 @@ strip-json-comments@^3.1.0, strip-json-comments@^3.1.1: resolved "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz" integrity sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig== +stripe@^13.2.0: + version "13.2.0" + resolved "https://registry.yarnpkg.com/stripe/-/stripe-13.2.0.tgz#feb10555d55c871188b0e9bc9bdf0f8e52c42e5d" + integrity sha512-4a2UHpe/tyxP3sxSGhuKMgbW8hQnqSQIPMigXC8kW3P0+BpsITpKDP+xxriTMDkRAP0xTQwzxcqhfqB+/404Mg== + dependencies: + "@types/node" ">=8.1.0" + qs "^6.11.0" + striptags@^2.0.3: version "2.2.1" resolved "https://registry.npmjs.org/striptags/-/striptags-2.2.1.tgz" @@ -9685,6 +9772,11 @@ toggle-selection@^1.0.6: resolved "https://registry.npmjs.org/toggle-selection/-/toggle-selection-1.0.6.tgz" integrity sha512-BiZS+C1OS8g/q2RRbJmy59xpyghNBqrr6k5L/uKBGRsTfxmu3ffiRnd8mlGPUVayg8pvfi5urfnu8TU7DVOkLQ== +toidentifier@1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/toidentifier/-/toidentifier-1.0.0.tgz#7e1be3470f1e77948bc43d94a3c8f4d7752ba553" + integrity sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw== + toposort@^2.0.2: version "2.0.2" resolved "https://registry.npmjs.org/toposort/-/toposort-2.0.2.tgz" @@ -10008,6 +10100,11 @@ universalify@^2.0.0: resolved "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz" integrity sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ== +unpipe@1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec" + integrity sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ== + unzipper@^0.10.11: version "0.10.11" resolved "https://registry.npmjs.org/unzipper/-/unzipper-0.10.11.tgz"