💸 Create abstraction for handling pricing (#1215)

This commit is contained in:
Luke Vella 2024-07-21 20:42:53 +01:00 committed by GitHub
parent 299b33df62
commit a5ee4fafe5
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
22 changed files with 302 additions and 68 deletions

View file

@ -64,6 +64,7 @@ jobs:
- name: Set environment variables - name: Set environment variables
run: | run: |
echo "DATABASE_URL=postgresql://postgres:postgres@localhost:5450/rallly" >> $GITHUB_ENV echo "DATABASE_URL=postgresql://postgres:postgres@localhost:5450/rallly" >> $GITHUB_ENV
echo "STRIPE_SECRET_KEY=${{ secrets.STRIPE_SECRET_KEY }}" >> $GITHUB_ENV
- name: Create production build - name: Create production build
run: yarn turbo build:test --filter=@rallly/web run: yarn turbo build:test --filter=@rallly/web

View file

@ -1,5 +1,6 @@
/// <reference types="next" /> /// <reference types="next" />
/// <reference types="next/image-types/global" /> /// <reference types="next/image-types/global" />
/// <reference types="next/navigation-types/compat/navigation" />
// NOTE: This file should not be edited // NOTE: This file should not be edited
// see https://nextjs.org/docs/basic-features/typescript for more information. // see https://nextjs.org/docs/basic-features/typescript for more information.

View file

@ -17,6 +17,7 @@
"@rallly/languages": "*", "@rallly/languages": "*",
"@rallly/tailwind-config": "*", "@rallly/tailwind-config": "*",
"@rallly/ui": "*", "@rallly/ui": "*",
"@rallly/billing": "*",
"@svgr/webpack": "^6.5.1", "@svgr/webpack": "^6.5.1",
"@vercel/analytics": "^0.1.8", "@vercel/analytics": "^0.1.8",
"class-variance-authority": "^0.7.0", "class-variance-authority": "^0.7.0",

View file

@ -3,7 +3,6 @@
"pricingDescription": "Get started for free. No login required.", "pricingDescription": "Get started for free. No login required.",
"freeForever": "free forever", "freeForever": "free forever",
"planPro": "Pro", "planPro": "Pro",
"annualBillingDescription": "per month, billed annually",
"monthlyBillingDescription": "per month", "monthlyBillingDescription": "per month",
"upgrade": "Upgrade", "upgrade": "Upgrade",
"faq": "Frequently Asked Questions", "faq": "Frequently Asked Questions",
@ -28,5 +27,7 @@
"planFree": "Free", "planFree": "Free",
"keepPollsIndefinitely": "Keep polls indefinitely", "keepPollsIndefinitely": "Keep polls indefinitely",
"whenPollInactive": "When does a poll become inactive?", "whenPollInactive": "When does a poll become inactive?",
"whenPollInactiveAnswer": "Polls become inactive when all date options are in the past AND the poll has not been accessed for over 30 days. Inactive polls are automatically deleted if you do not have a paid subscription." "whenPollInactiveAnswer": "Polls become inactive when all date options are in the past AND the poll has not been accessed for over 30 days. Inactive polls are automatically deleted if you do not have a paid subscription.",
"discount": "Save {amount}",
"yearlyBillingDescription": "per year"
} }

View file

@ -1,3 +1,6 @@
import type { PricingData } from "@rallly/billing";
import { getProPricing } from "@rallly/billing";
import { Badge } from "@rallly/ui/badge";
import { import {
BillingPlan, BillingPlan,
BillingPlanDescription, BillingPlanDescription,
@ -11,6 +14,7 @@ import {
import { Button } from "@rallly/ui/button"; import { Button } from "@rallly/ui/button";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@rallly/ui/tabs"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@rallly/ui/tabs";
import { TrendingUpIcon } from "lucide-react"; import { TrendingUpIcon } from "lucide-react";
import { GetStaticProps } from "next";
import Link from "next/link"; import Link from "next/link";
import { useTranslation } from "next-i18next"; import { useTranslation } from "next-i18next";
import { NextSeo } from "next-seo"; import { NextSeo } from "next-seo";
@ -22,9 +26,6 @@ import { linkToApp } from "@/lib/linkToApp";
import { NextPageWithLayout } from "@/types"; import { NextPageWithLayout } from "@/types";
import { getStaticTranslations } from "@/utils/page-translations"; import { getStaticTranslations } from "@/utils/page-translations";
const monthlyPriceUsd = 7;
const annualPriceUsd = 42;
export const UpgradeButton = ({ export const UpgradeButton = ({
children, children,
annual, annual,
@ -48,7 +49,7 @@ export const UpgradeButton = ({
); );
}; };
const PriceTables = () => { const PriceTables = ({ pricingData }: { pricingData: PricingData }) => {
const [tab, setTab] = React.useState("yearly"); const [tab, setTab] = React.useState("yearly");
return ( return (
<Tabs value={tab} onValueChange={setTab}> <Tabs value={tab} onValueChange={setTab}>
@ -57,8 +58,17 @@ const PriceTables = () => {
<TabsTrigger value="monthly"> <TabsTrigger value="monthly">
<Trans i18nKey="pricing:billingPeriodMonthly" defaults="Monthly" /> <Trans i18nKey="pricing:billingPeriodMonthly" defaults="Monthly" />
</TabsTrigger> </TabsTrigger>
<TabsTrigger value="yearly"> <TabsTrigger value="yearly" className="inline-flex gap-x-2.5">
<Trans i18nKey="pricing:billingPeriodYearly" defaults="Yearly" /> <Trans i18nKey="pricing:billingPeriodYearly" defaults="Yearly" />
<Badge variant="green" className="inline-flex gap-2">
<Trans
i18nKey="pricing:discount"
defaults="Save {amount}"
values={{
amount: `$${(pricingData.monthly.amount * 12 - pricingData.yearly.amount) / 100}`,
}}
/>
</Badge>
</TabsTrigger> </TabsTrigger>
</TabsList> </TabsList>
</div> </div>
@ -115,18 +125,20 @@ const PriceTables = () => {
</BillingPlanDescription> </BillingPlanDescription>
</BillingPlanHeader> </BillingPlanHeader>
<TabsContent value="yearly"> <TabsContent value="yearly">
<BillingPlanPrice discount={`$${(annualPriceUsd / 12).toFixed(2)}`}> <BillingPlanPrice>
${monthlyPriceUsd} ${pricingData.yearly.amount / 100}
</BillingPlanPrice> </BillingPlanPrice>
<BillingPlanPeriod> <BillingPlanPeriod>
<Trans <Trans
i18nKey="pricing:annualBillingDescription" i18nKey="pricing:yearlyBillingDescription"
defaults="per month, billed annually" defaults="per year"
/> />
</BillingPlanPeriod> </BillingPlanPeriod>
</TabsContent> </TabsContent>
<TabsContent value="monthly"> <TabsContent value="monthly">
<BillingPlanPrice>${monthlyPriceUsd}</BillingPlanPrice> <BillingPlanPrice>
${pricingData.monthly.amount / 100}
</BillingPlanPrice>
<BillingPlanPeriod> <BillingPlanPeriod>
<Trans <Trans
i18nKey="pricing:monthlyBillingDescription" i18nKey="pricing:monthlyBillingDescription"
@ -266,7 +278,9 @@ const FAQ = () => {
); );
}; };
const Page: NextPageWithLayout = () => { const Page: NextPageWithLayout<{ pricingData: PricingData }> = ({
pricingData,
}) => {
const { t } = useTranslation(["pricing"]); const { t } = useTranslation(["pricing"]);
return ( return (
<div className="mx-auto max-w-3xl"> <div className="mx-auto max-w-3xl">
@ -286,7 +300,7 @@ const Page: NextPageWithLayout = () => {
</p> </p>
</div> </div>
<div className="space-y-4 sm:space-y-6"> <div className="space-y-4 sm:space-y-6">
<PriceTables /> <PriceTables pricingData={pricingData} />
<div className="rounded-md border bg-gradient-to-b from-cyan-50 to-cyan-50/60 px-5 py-4 text-cyan-800"> <div className="rounded-md border bg-gradient-to-b from-cyan-50 to-cyan-50/60 px-5 py-4 text-cyan-800">
<div className="mb-2"> <div className="mb-2">
<TrendingUpIcon className="text-indigo mr-2 mt-0.5 size-6 shrink-0" /> <TrendingUpIcon className="text-indigo mr-2 mt-0.5 size-6 shrink-0" />
@ -317,4 +331,16 @@ Page.getLayout = getPageLayout;
export default Page; export default Page;
export const getStaticProps = getStaticTranslations(["pricing"]); export const getStaticProps: GetStaticProps = async (ctx) => {
const pricingData = await getProPricing();
const res = await getStaticTranslations(["pricing"])(ctx);
if ("props" in res) {
return {
props: {
...res.props,
pricingData,
},
};
}
return res;
};

View file

@ -3,11 +3,23 @@
"compilerOptions": { "compilerOptions": {
"baseUrl": ".", "baseUrl": ".",
"paths": { "paths": {
"@/*": ["src/*"], "@/*": [
"~/*": ["public/*"], "src/*"
],
"~/*": [
"public/*"
]
}, },
"checkJs": false, "checkJs": false,
"strictNullChecks": true
}, },
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], "include": [
"exclude": ["node_modules"], "next-env.d.ts",
"**/*.ts",
"**/*.tsx",
".next/types/**/*.ts"
],
"exclude": [
"node_modules"
]
} }

View file

@ -27,6 +27,7 @@
"@rallly/backend": "*", "@rallly/backend": "*",
"@rallly/database": "*", "@rallly/database": "*",
"@rallly/icons": "*", "@rallly/icons": "*",
"@rallly/billing": "*",
"@rallly/languages": "*", "@rallly/languages": "*",
"@rallly/tailwind-config": "*", "@rallly/tailwind-config": "*",
"@rallly/ui": "*", "@rallly/ui": "*",

View file

@ -139,7 +139,6 @@
"billingStatus": "Billing Status", "billingStatus": "Billing Status",
"billingStatusDescription": "Manage your subscription and billing details", "billingStatusDescription": "Manage your subscription and billing details",
"freeForever": "free forever", "freeForever": "free forever",
"annualBillingDescription": "per month, billed annually",
"billingStatusState": "Status", "billingStatusState": "Status",
"billingStatusActive": "Active", "billingStatusActive": "Active",
"billingStatusPaused": "Paused", "billingStatusPaused": "Paused",
@ -228,7 +227,6 @@
"autoTimeZoneHelp": "Enable this setting to automatically adjust event times to each participant's local time zone.", "autoTimeZoneHelp": "Enable this setting to automatically adjust event times to each participant's local time zone.",
"commentsDisabled": "Comments have been disabled", "commentsDisabled": "Comments have been disabled",
"allParticipants": "All Participants", "allParticipants": "All Participants",
"pollsListAll": "All",
"noParticipantsDescription": "Click <b>Share</b> to invite participants", "noParticipantsDescription": "Click <b>Share</b> to invite participants",
"timeShownIn": "Times shown in {timeZone}", "timeShownIn": "Times shown in {timeZone}",
"pollStatusPausedDescription": "Votes cannot be submitted or edited at this time", "pollStatusPausedDescription": "Votes cannot be submitted or edited at this time",
@ -263,5 +261,7 @@
"pastEventsEmptyStateTitle": "No Past Events", "pastEventsEmptyStateTitle": "No Past Events",
"pastEventsEmptyStateDescription": "When you schedule events, they will appear here.", "pastEventsEmptyStateDescription": "When you schedule events, they will appear here.",
"activePollCount": "{{activePollCount}} Live", "activePollCount": "{{activePollCount}} Live",
"createPoll": "Create poll" "createPoll": "Create poll",
"yearlyDiscount": "Save {amount}",
"yearlyBillingDescription": "per year"
} }

View file

@ -4,12 +4,9 @@ import { Card } from "@rallly/ui/card";
import { Label } from "@rallly/ui/label"; import { Label } from "@rallly/ui/label";
import dayjs from "dayjs"; import dayjs from "dayjs";
import { ArrowUpRight, CreditCardIcon, SendIcon } from "lucide-react"; import { ArrowUpRight, CreditCardIcon, SendIcon } from "lucide-react";
import Head from "next/head";
import Link from "next/link"; import Link from "next/link";
import Script from "next/script"; import Script from "next/script";
import { useTranslation } from "next-i18next";
import { BillingPlans } from "@/components/billing/billing-plans";
import { import {
Settings, Settings,
SettingsContent, SettingsContent,
@ -19,6 +16,8 @@ import { Trans } from "@/components/trans";
import { useSubscription } from "@/contexts/plan"; import { useSubscription } from "@/contexts/plan";
import { trpc } from "@/utils/trpc/client"; import { trpc } from "@/utils/trpc/client";
import { BillingPlans, PricingData } from "./billing-plans";
declare global { declare global {
interface Window { interface Window {
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
@ -57,7 +56,7 @@ const BillingPortal = () => {
const proPlanIdMonthly = process.env.NEXT_PUBLIC_PRO_PLAN_ID_MONTHLY as string; const proPlanIdMonthly = process.env.NEXT_PUBLIC_PRO_PLAN_ID_MONTHLY as string;
const SubscriptionStatus = () => { const SubscriptionStatus = ({ pricingData }: { pricingData: PricingData }) => {
const data = useSubscription(); const data = useSubscription();
if (!data) { if (!data) {
@ -67,7 +66,7 @@ const SubscriptionStatus = () => {
return ( return (
<div className="space-y-6"> <div className="space-y-6">
{!data.active ? ( {!data.active ? (
<BillingPlans /> <BillingPlans pricingData={pricingData} />
) : data.legacy ? ( ) : data.legacy ? (
<LegacyBilling /> <LegacyBilling />
) : ( ) : (
@ -238,16 +237,11 @@ const LegacyBilling = () => {
</div> </div>
</SettingsSection>; </SettingsSection>;
export function BillingPage() { export function BillingPage({ pricingData }: { pricingData: PricingData }) {
const { t } = useTranslation();
return ( return (
<Settings> <Settings>
<Head>
<title>{t("billing")}</title>
</Head>
<SettingsContent> <SettingsContent>
<SubscriptionStatus /> <SubscriptionStatus pricingData={pricingData} />
<hr /> <hr />
<SettingsSection <SettingsSection
title={<Trans i18nKey="support" defaults="Support" />} title={<Trans i18nKey="support" defaults="Support" />}

View file

@ -1,3 +1,4 @@
import { Badge } from "@rallly/ui/badge";
import { import {
BillingPlan, BillingPlan,
BillingPlanDescription, BillingPlanDescription,
@ -15,9 +16,21 @@ import React from "react";
import { Trans } from "@/components/trans"; import { Trans } from "@/components/trans";
import { UpgradeButton } from "@/components/upgrade-button"; import { UpgradeButton } from "@/components/upgrade-button";
import { annualPriceUsd, monthlyPriceUsd } from "@/utils/constants";
export const BillingPlans = () => { export type PricingData = {
monthly: {
id: string;
amount: number;
currency: string;
};
yearly: {
id: string;
amount: number;
currency: string;
};
};
export const BillingPlans = ({ pricingData }: { pricingData: PricingData }) => {
const [tab, setTab] = React.useState("yearly"); const [tab, setTab] = React.useState("yearly");
return ( return (
@ -28,8 +41,21 @@ export const BillingPlans = () => {
<TabsTrigger value="monthly"> <TabsTrigger value="monthly">
<Trans i18nKey="billingPeriodMonthly" /> <Trans i18nKey="billingPeriodMonthly" />
</TabsTrigger> </TabsTrigger>
<TabsTrigger value="yearly"> <TabsTrigger value="yearly" className="inline-flex gap-x-2.5">
<Trans i18nKey="billingPeriodYearly" /> <Trans i18nKey="billingPeriodYearly" />
<Badge variant="green">
<Trans
i18nKey="yearlyDiscount"
defaults="Save {amount}"
values={{
amount: `$${
(pricingData.monthly.amount * 12 -
pricingData.yearly.amount) /
100
}`,
}}
/>
</Badge>
</TabsTrigger> </TabsTrigger>
</TabsList> </TabsList>
</div> </div>
@ -85,20 +111,20 @@ export const BillingPlans = () => {
</div> </div>
<div className="flex"> <div className="flex">
<TabsContent value="yearly"> <TabsContent value="yearly">
<BillingPlanPrice <BillingPlanPrice>
discount={`$${(annualPriceUsd / 12).toFixed(2)}`} ${pricingData.yearly.amount / 100}
>
${monthlyPriceUsd}
</BillingPlanPrice> </BillingPlanPrice>
<BillingPlanPeriod> <BillingPlanPeriod>
<Trans <Trans
i18nKey="annualBillingDescription" i18nKey="yearlyBillingDescription"
defaults="per month, billed annually" defaults="per year"
/> />
</BillingPlanPeriod> </BillingPlanPeriod>
</TabsContent> </TabsContent>
<TabsContent value="monthly"> <TabsContent value="monthly">
<BillingPlanPrice>${monthlyPriceUsd}</BillingPlanPrice> <BillingPlanPrice>
${pricingData.monthly.amount / 100}
</BillingPlanPrice>
<BillingPlanPeriod> <BillingPlanPeriod>
<Trans <Trans
i18nKey="monthlyBillingDescription" i18nKey="monthlyBillingDescription"

View file

@ -1,10 +1,17 @@
import { getProPricing } from "@rallly/billing";
import { notFound } from "next/navigation";
import { BillingPage } from "@/app/[locale]/(admin)/settings/billing/billing-page";
import { Params } from "@/app/[locale]/types"; import { Params } from "@/app/[locale]/types";
import { getTranslation } from "@/app/i18n"; import { getTranslation } from "@/app/i18n";
import { env } from "@/env";
import { BillingPage } from "./billing-page";
export default async function Page() { export default async function Page() {
return <BillingPage />; if (env.NEXT_PUBLIC_SELF_HOSTED === "true") {
notFound();
}
const prices = await getProPricing();
return <BillingPage pricingData={prices} />;
} }
export async function generateMetadata({ params }: { params: Params }) { export async function generateMetadata({ params }: { params: Params }) {

View file

@ -58,6 +58,7 @@ export const env = createEnv({
client: { client: {
NEXT_PUBLIC_POSTHOG_API_KEY: z.string().optional(), NEXT_PUBLIC_POSTHOG_API_KEY: z.string().optional(),
NEXT_PUBLIC_POSTHOG_API_HOST: z.string().url().optional(), NEXT_PUBLIC_POSTHOG_API_HOST: z.string().url().optional(),
NEXT_PUBLIC_SELF_HOSTED: z.enum(["true", "false"]).optional(),
}, },
/* /*
* Due to how Next.js bundles environment variables on Edge and Client, * Due to how Next.js bundles environment variables on Edge and Client,
@ -88,6 +89,7 @@ export const env = createEnv({
ALLOWED_EMAILS: process.env.ALLOWED_EMAILS, ALLOWED_EMAILS: process.env.ALLOWED_EMAILS,
NEXT_PUBLIC_POSTHOG_API_KEY: process.env.NEXT_PUBLIC_POSTHOG_API_KEY, NEXT_PUBLIC_POSTHOG_API_KEY: process.env.NEXT_PUBLIC_POSTHOG_API_KEY,
NEXT_PUBLIC_POSTHOG_API_HOST: process.env.NEXT_PUBLIC_POSTHOG_API_HOST, NEXT_PUBLIC_POSTHOG_API_HOST: process.env.NEXT_PUBLIC_POSTHOG_API_HOST,
NEXT_PUBLIC_SELF_HOSTED: process.env.NEXT_PUBLIC_SELF_HOSTED,
}, },
skipValidation: !!process.env.SKIP_ENV_VALIDATION, skipValidation: !!process.env.SKIP_ENV_VALIDATION,
}); });

View file

@ -1,4 +1,4 @@
import { stripe } from "@rallly/backend/stripe"; import { getProPricing, stripe } from "@rallly/billing";
import { prisma } from "@rallly/database"; import { prisma } from "@rallly/database";
import { NextApiRequest, NextApiResponse } from "next"; import { NextApiRequest, NextApiResponse } from "next";
import { z } from "zod"; import { z } from "zod";
@ -61,6 +61,8 @@ export default async function handler(
return; return;
} }
const proPricingData = await getProPricing();
const session = await stripe.checkout.sessions.create({ const session = await stripe.checkout.sessions.create({
success_url: absoluteUrl( success_url: absoluteUrl(
return_path ?? "/api/stripe/portal?session_id={CHECKOUT_SESSION_ID}", return_path ?? "/api/stripe/portal?session_id={CHECKOUT_SESSION_ID}",
@ -97,8 +99,8 @@ export default async function handler(
{ {
price: price:
period === "yearly" period === "yearly"
? (process.env.STRIPE_YEARLY_PRICE as string) ? proPricingData.yearly.id
: (process.env.STRIPE_MONTHLY_PRICE as string), : proPricingData.monthly.id,
quantity: 1, quantity: 1,
}, },
], ],

View file

@ -8,7 +8,4 @@ export const isSelfHosted = process.env.NEXT_PUBLIC_SELF_HOSTED === "true";
export const isFeedbackEnabled = false; export const isFeedbackEnabled = false;
export const monthlyPriceUsd = 7;
export const annualPriceUsd = 42;
export const appVersion = process.env.NEXT_PUBLIC_APP_VERSION; export const appVersion = process.env.NEXT_PUBLIC_APP_VERSION;

View file

@ -0,0 +1,16 @@
{
"name": "@rallly/billing",
"version": "0.0.0",
"private": true,
"exports": {
"./server/*": "./src/server/*.tsx",
"./next": "./src/next/index.ts",
".": "./src/index.ts"
},
"dependencies": {
"@rallly/ui": "*",
"stripe": "^13.2.0",
"@radix-ui/react-radio-group": "^1.2.0",
"next": "*"
}
}

View file

@ -0,0 +1 @@
export * from "./lib/stripe";

View file

@ -0,0 +1,20 @@
import { stripe } from "..";
export async function getPricing() {
const prices = await stripe.prices.list({
lookup_keys: ["pro-monthly", "pro-yearly"],
});
const [monthly, yearly] = prices.data;
return {
monthly: {
currency: monthly.currency,
price: monthly.unit_amount_decimal,
},
yearly: {
currency: yearly.currency,
price: yearly.unit_amount,
},
};
}

View file

@ -0,0 +1,35 @@
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,
});
export async function getProPricing() {
const prices = await stripe.prices.list({
lookup_keys: ["pro-monthly", "pro-yearly"],
});
const [monthly, yearly] = prices.data;
if (monthly.unit_amount === null || yearly.unit_amount === null) {
throw new Error("Price not found");
}
return {
monthly: {
id: monthly.id,
amount: monthly.unit_amount,
currency: monthly.currency,
},
yearly: {
id: yearly.id,
amount: yearly.unit_amount,
currency: yearly.currency,
},
};
}
export type PricingData = Awaited<ReturnType<typeof getProPricing>>;

View file

@ -0,0 +1,23 @@
import { NextRequest, NextResponse } from "next/server";
import { getPricing } from "../lib/get-pricing";
export async function GET(
request: NextRequest,
{
params,
}: {
params: {
method: string;
};
},
) {
switch (params.method) {
case "pricing":
const data = await getPricing();
return NextResponse.json(data);
default:
return NextResponse.json({ message: "Method not found" });
}
}
export const handlers = { GET };

View file

@ -0,0 +1,5 @@
{
"extends": "@rallly/tsconfig/next.json",
"include": ["**/*.ts", "**/*.tsx", "**/*.js"],
"exclude": ["node_modules"],
}

View file

@ -37,20 +37,10 @@ export const BillingPlanDescription = ({
export const BillingPlanPrice = ({ export const BillingPlanPrice = ({
children, children,
discount,
}: React.PropsWithChildren<{ discount?: React.ReactNode }>) => { }: React.PropsWithChildren<{ discount?: React.ReactNode }>) => {
return ( return (
<div> <div className="flex items-center gap-4">
{discount ? ( <span className="text-3xl font-bold">{children}</span>
<>
<span className="mr-2 text-xl font-bold line-through">
{children}
</span>
<span className="text-3xl font-bold">{discount}</span>
</>
) : (
<span className="text-3xl font-bold">{children}</span>
)}
</div> </div>
); );
}; };

View file

@ -2293,6 +2293,11 @@
resolved "https://registry.yarnpkg.com/@next/env/-/env-14.0.5-canary.46.tgz#b9b597baaba77a2836eaf836712a6e0afed1ca2d" resolved "https://registry.yarnpkg.com/@next/env/-/env-14.0.5-canary.46.tgz#b9b597baaba77a2836eaf836712a6e0afed1ca2d"
integrity sha512-dvNzrArTfe3VY1VIscpb3E2e7SZ1qwFe82WGzpOVbxilT3JcsnVGYF/uq8Jj1qKWPI5C/aePNXwA97JRNAXpRQ== integrity sha512-dvNzrArTfe3VY1VIscpb3E2e7SZ1qwFe82WGzpOVbxilT3JcsnVGYF/uq8Jj1qKWPI5C/aePNXwA97JRNAXpRQ==
"@next/env@14.1.0":
version "14.1.0"
resolved "https://registry.yarnpkg.com/@next/env/-/env-14.1.0.tgz#43d92ebb53bc0ae43dcc64fb4d418f8f17d7a341"
integrity sha512-Py8zIo+02ht82brwwhTg36iogzFqGLPXlRGKQw5s+qP/kMNc4MAyDeEwBKDijk6zTIbegEgu8Qy7C1LboslQAw==
"@next/env@14.2.4": "@next/env@14.2.4":
version "14.2.4" version "14.2.4"
resolved "https://registry.yarnpkg.com/@next/env/-/env-14.2.4.tgz#5546813dc4f809884a37d257b254a5ce1b0248d7" resolved "https://registry.yarnpkg.com/@next/env/-/env-14.2.4.tgz#5546813dc4f809884a37d257b254a5ce1b0248d7"
@ -2310,6 +2315,11 @@
resolved "https://registry.yarnpkg.com/@next/swc-darwin-arm64/-/swc-darwin-arm64-14.0.5-canary.46.tgz#94c67fa212614892f94db120c92a9f4207da13b8" resolved "https://registry.yarnpkg.com/@next/swc-darwin-arm64/-/swc-darwin-arm64-14.0.5-canary.46.tgz#94c67fa212614892f94db120c92a9f4207da13b8"
integrity sha512-7Bq9rjWl4sq70Zkn6h6mn8/tgYTH2SQ8lIm8b/j1MAnTiJYyVBLapu//gT/cgtqx6y8SwSc2JNviBue35zeCNw== integrity sha512-7Bq9rjWl4sq70Zkn6h6mn8/tgYTH2SQ8lIm8b/j1MAnTiJYyVBLapu//gT/cgtqx6y8SwSc2JNviBue35zeCNw==
"@next/swc-darwin-arm64@14.1.0":
version "14.1.0"
resolved "https://registry.yarnpkg.com/@next/swc-darwin-arm64/-/swc-darwin-arm64-14.1.0.tgz#70a57c87ab1ae5aa963a3ba0f4e59e18f4ecea39"
integrity sha512-nUDn7TOGcIeyQni6lZHfzNoo9S0euXnu0jhsbMOmMJUBfgsnESdjN97kM7cBqQxZa8L/bM9om/S5/1dzCrW6wQ==
"@next/swc-darwin-arm64@14.2.4": "@next/swc-darwin-arm64@14.2.4":
version "14.2.4" version "14.2.4"
resolved "https://registry.yarnpkg.com/@next/swc-darwin-arm64/-/swc-darwin-arm64-14.2.4.tgz#da9f04c34a3d5f0b8401ed745768420e4a604036" resolved "https://registry.yarnpkg.com/@next/swc-darwin-arm64/-/swc-darwin-arm64-14.2.4.tgz#da9f04c34a3d5f0b8401ed745768420e4a604036"
@ -2320,6 +2330,11 @@
resolved "https://registry.yarnpkg.com/@next/swc-darwin-x64/-/swc-darwin-x64-14.0.5-canary.46.tgz#25e2a5acfc5b20d3a25ad6adcfbfc91aaa44d79f" resolved "https://registry.yarnpkg.com/@next/swc-darwin-x64/-/swc-darwin-x64-14.0.5-canary.46.tgz#25e2a5acfc5b20d3a25ad6adcfbfc91aaa44d79f"
integrity sha512-3oI8rDVBZsfkTdqXwtRjxA85o0RIjZv9uuOLohfaIuFP3oZnCM0dRZREP2umYcFQRxdavW+TDJzYcqzKxYTujA== integrity sha512-3oI8rDVBZsfkTdqXwtRjxA85o0RIjZv9uuOLohfaIuFP3oZnCM0dRZREP2umYcFQRxdavW+TDJzYcqzKxYTujA==
"@next/swc-darwin-x64@14.1.0":
version "14.1.0"
resolved "https://registry.yarnpkg.com/@next/swc-darwin-x64/-/swc-darwin-x64-14.1.0.tgz#0863a22feae1540e83c249384b539069fef054e9"
integrity sha512-1jgudN5haWxiAl3O1ljUS2GfupPmcftu2RYJqZiMJmmbBT5M1XDffjUtRUzP4W3cBHsrvkfOFdQ71hAreNQP6g==
"@next/swc-darwin-x64@14.2.4": "@next/swc-darwin-x64@14.2.4":
version "14.2.4" version "14.2.4"
resolved "https://registry.yarnpkg.com/@next/swc-darwin-x64/-/swc-darwin-x64-14.2.4.tgz#46dedb29ec5503bf171a72a3ecb8aac6e738e9d6" resolved "https://registry.yarnpkg.com/@next/swc-darwin-x64/-/swc-darwin-x64-14.2.4.tgz#46dedb29ec5503bf171a72a3ecb8aac6e738e9d6"
@ -2330,6 +2345,11 @@
resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-14.0.5-canary.46.tgz#00fad5be6cada895e513d81427c462c92abdae3f" resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-14.0.5-canary.46.tgz#00fad5be6cada895e513d81427c462c92abdae3f"
integrity sha512-gXSS328bUWxBwQfeDFROOzFSzzoyX1075JxOeArLl63sV59cbnRrwHHhD4CWG1bYYzcHxHfVugZgvyCucaHCIw== integrity sha512-gXSS328bUWxBwQfeDFROOzFSzzoyX1075JxOeArLl63sV59cbnRrwHHhD4CWG1bYYzcHxHfVugZgvyCucaHCIw==
"@next/swc-linux-arm64-gnu@14.1.0":
version "14.1.0"
resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-14.1.0.tgz#893da533d3fce4aec7116fe772d4f9b95232423c"
integrity sha512-RHo7Tcj+jllXUbK7xk2NyIDod3YcCPDZxj1WLIYxd709BQ7WuRYl3OWUNG+WUfqeQBds6kvZYlc42NJJTNi4tQ==
"@next/swc-linux-arm64-gnu@14.2.4": "@next/swc-linux-arm64-gnu@14.2.4":
version "14.2.4" version "14.2.4"
resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-14.2.4.tgz#c9697ab9eb422bd1d7ffd0eb0779cc2aefa9d4a1" resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-14.2.4.tgz#c9697ab9eb422bd1d7ffd0eb0779cc2aefa9d4a1"
@ -2340,6 +2360,11 @@
resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-14.0.5-canary.46.tgz#a931a1312d3f5e66ea59c4b23e0ae90721f8e252" resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-14.0.5-canary.46.tgz#a931a1312d3f5e66ea59c4b23e0ae90721f8e252"
integrity sha512-7QkBRKlDsjaWGbfIKh6qJK0HiHJISNGoKpwFTcnZvlhAEaydS5Hmu0zh64kbLRlzwXtkpj6/iCwjrWnHes59aA== integrity sha512-7QkBRKlDsjaWGbfIKh6qJK0HiHJISNGoKpwFTcnZvlhAEaydS5Hmu0zh64kbLRlzwXtkpj6/iCwjrWnHes59aA==
"@next/swc-linux-arm64-musl@14.1.0":
version "14.1.0"
resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-14.1.0.tgz#d81ddcf95916310b8b0e4ad32b637406564244c0"
integrity sha512-v6kP8sHYxjO8RwHmWMJSq7VZP2nYCkRVQ0qolh2l6xroe9QjbgV8siTbduED4u0hlk0+tjS6/Tuy4n5XCp+l6g==
"@next/swc-linux-arm64-musl@14.2.4": "@next/swc-linux-arm64-musl@14.2.4":
version "14.2.4" version "14.2.4"
resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-14.2.4.tgz#cbbceb2008571c743b5a310a488d2e166d200a75" resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-14.2.4.tgz#cbbceb2008571c743b5a310a488d2e166d200a75"
@ -2350,6 +2375,11 @@
resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-14.0.5-canary.46.tgz#32bf69fa93975ca3fef141121eaa8a1a67086694" resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-14.0.5-canary.46.tgz#32bf69fa93975ca3fef141121eaa8a1a67086694"
integrity sha512-DS5wTjw3FtcLFVzRxLMJgmDNMoeaXp5qBdKUSBrKTq4zQnqUi99CGz2461DlUSxJCWPUgAVo23MdoQD6Siuk7A== integrity sha512-DS5wTjw3FtcLFVzRxLMJgmDNMoeaXp5qBdKUSBrKTq4zQnqUi99CGz2461DlUSxJCWPUgAVo23MdoQD6Siuk7A==
"@next/swc-linux-x64-gnu@14.1.0":
version "14.1.0"
resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-14.1.0.tgz#18967f100ec19938354332dcb0268393cbacf581"
integrity sha512-zJ2pnoFYB1F4vmEVlb/eSe+VH679zT1VdXlZKX+pE66grOgjmKJHKacf82g/sWE4MQ4Rk2FMBCRnX+l6/TVYzQ==
"@next/swc-linux-x64-gnu@14.2.4": "@next/swc-linux-x64-gnu@14.2.4":
version "14.2.4" version "14.2.4"
resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-14.2.4.tgz#d79184223f857bacffb92f643cb2943a43632568" resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-14.2.4.tgz#d79184223f857bacffb92f643cb2943a43632568"
@ -2360,6 +2390,11 @@
resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-14.0.5-canary.46.tgz#59f221d83096b0362849fabbcda1fdc1671cf6b1" resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-14.0.5-canary.46.tgz#59f221d83096b0362849fabbcda1fdc1671cf6b1"
integrity sha512-d409ur5JGj6HFp8DBu5M2oTh5EddDcrT+vjewQkAq/A7MZoAMAOH74xOFouEnJs0/dQ71XvH9Lw+1gJSnElcyQ== integrity sha512-d409ur5JGj6HFp8DBu5M2oTh5EddDcrT+vjewQkAq/A7MZoAMAOH74xOFouEnJs0/dQ71XvH9Lw+1gJSnElcyQ==
"@next/swc-linux-x64-musl@14.1.0":
version "14.1.0"
resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-14.1.0.tgz#77077cd4ba8dda8f349dc7ceb6230e68ee3293cf"
integrity sha512-rbaIYFt2X9YZBSbH/CwGAjbBG2/MrACCVu2X0+kSykHzHnYH5FjHxwXLkcoJ10cX0aWCEynpu+rP76x0914atg==
"@next/swc-linux-x64-musl@14.2.4": "@next/swc-linux-x64-musl@14.2.4":
version "14.2.4" version "14.2.4"
resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-14.2.4.tgz#6b6c3e5ac02ca5e63394d280ec8ee607491902df" resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-14.2.4.tgz#6b6c3e5ac02ca5e63394d280ec8ee607491902df"
@ -2370,6 +2405,11 @@
resolved "https://registry.yarnpkg.com/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-14.0.5-canary.46.tgz#465d24227cd1b8840b85ee488327725e478da221" resolved "https://registry.yarnpkg.com/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-14.0.5-canary.46.tgz#465d24227cd1b8840b85ee488327725e478da221"
integrity sha512-goyh/RCFtivflIOvbwircMxTSObETufm3pcxtI8rIz9+pg/M2MmK8/z48EZybkEcPKl41xu4s1iqXThy/jDPng== integrity sha512-goyh/RCFtivflIOvbwircMxTSObETufm3pcxtI8rIz9+pg/M2MmK8/z48EZybkEcPKl41xu4s1iqXThy/jDPng==
"@next/swc-win32-arm64-msvc@14.1.0":
version "14.1.0"
resolved "https://registry.yarnpkg.com/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-14.1.0.tgz#5f0b8cf955644104621e6d7cc923cad3a4c5365a"
integrity sha512-o1N5TsYc8f/HpGt39OUQpQ9AKIGApd3QLueu7hXk//2xq5Z9OxmV6sQfNp8C7qYmiOlHYODOGqNNa0e9jvchGQ==
"@next/swc-win32-arm64-msvc@14.2.4": "@next/swc-win32-arm64-msvc@14.2.4":
version "14.2.4" version "14.2.4"
resolved "https://registry.yarnpkg.com/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-14.2.4.tgz#dbad3906e870dba84c5883d9d4c4838472e0697f" resolved "https://registry.yarnpkg.com/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-14.2.4.tgz#dbad3906e870dba84c5883d9d4c4838472e0697f"
@ -2380,6 +2420,11 @@
resolved "https://registry.yarnpkg.com/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-14.0.5-canary.46.tgz#0a65de42dcb8a8293ee0f8e3082d4d8c326f3d12" resolved "https://registry.yarnpkg.com/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-14.0.5-canary.46.tgz#0a65de42dcb8a8293ee0f8e3082d4d8c326f3d12"
integrity sha512-SEnrOZ7ASXdd/GBq2x0IfpSbfamv1rZfcDeZZLF7kzu0pY7jDQwcW8zTKwwC8JH5CLGLfI3wD6wUYrA+PgJSCw== integrity sha512-SEnrOZ7ASXdd/GBq2x0IfpSbfamv1rZfcDeZZLF7kzu0pY7jDQwcW8zTKwwC8JH5CLGLfI3wD6wUYrA+PgJSCw==
"@next/swc-win32-ia32-msvc@14.1.0":
version "14.1.0"
resolved "https://registry.yarnpkg.com/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-14.1.0.tgz#21f4de1293ac5e5a168a412b139db5d3420a89d0"
integrity sha512-XXIuB1DBRCFwNO6EEzCTMHT5pauwaSj4SWs7CYnME57eaReAKBXCnkUE80p/pAZcewm7hs+vGvNqDPacEXHVkw==
"@next/swc-win32-ia32-msvc@14.2.4": "@next/swc-win32-ia32-msvc@14.2.4":
version "14.2.4" version "14.2.4"
resolved "https://registry.yarnpkg.com/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-14.2.4.tgz#6074529b91ba49132922ce89a2e16d25d2ec235d" resolved "https://registry.yarnpkg.com/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-14.2.4.tgz#6074529b91ba49132922ce89a2e16d25d2ec235d"
@ -2390,6 +2435,11 @@
resolved "https://registry.yarnpkg.com/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-14.0.5-canary.46.tgz#e19326097b306c58eb47984acf7f7eca4485b604" resolved "https://registry.yarnpkg.com/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-14.0.5-canary.46.tgz#e19326097b306c58eb47984acf7f7eca4485b604"
integrity sha512-NK1EJLyeUxgX9IHSxO0kN1Nk8VsaDfjHVYL4p9fM24e/9rG8jPcxquIQJ4Wy+ZdqxaVivqQ2eHrJYUpXpfOXmw== integrity sha512-NK1EJLyeUxgX9IHSxO0kN1Nk8VsaDfjHVYL4p9fM24e/9rG8jPcxquIQJ4Wy+ZdqxaVivqQ2eHrJYUpXpfOXmw==
"@next/swc-win32-x64-msvc@14.1.0":
version "14.1.0"
resolved "https://registry.yarnpkg.com/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-14.1.0.tgz#e561fb330466d41807123d932b365cf3d33ceba2"
integrity sha512-9WEbVRRAqJ3YFVqEZIxUqkiO8l1nool1LmNxygr5HWF8AcSYsEpneUDhmjUVJEzO2A04+oPtZdombzzPPkTtgg==
"@next/swc-win32-x64-msvc@14.2.4": "@next/swc-win32-x64-msvc@14.2.4":
version "14.2.4" version "14.2.4"
resolved "https://registry.yarnpkg.com/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-14.2.4.tgz#e65a1c6539a671f97bb86d5183d6e3a1733c29c7" resolved "https://registry.yarnpkg.com/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-14.2.4.tgz#e65a1c6539a671f97bb86d5183d6e3a1733c29c7"
@ -6344,9 +6394,9 @@ caniuse-lite@^1.0.30001406, caniuse-lite@^1.0.30001426, caniuse-lite@^1.0.300014
integrity sha512-c3dl911slnQhmxUIT4HhYzT7wnBK/XYpGnYLOj4nJBaRiw52Ibe7YxlDaAeRECvA786zCuExhxIUJ2K7nHMrBw== integrity sha512-c3dl911slnQhmxUIT4HhYzT7wnBK/XYpGnYLOj4nJBaRiw52Ibe7YxlDaAeRECvA786zCuExhxIUJ2K7nHMrBw==
caniuse-lite@^1.0.30001579: caniuse-lite@^1.0.30001579:
version "1.0.30001633" version "1.0.30001583"
resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001633.tgz#45a4ade9fb9ec80a06537a6271ac1e0afadcb324" resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001583.tgz#abb2970cc370801dc7e27bf290509dc132cfa390"
integrity sha512-6sT0yf/z5jqf8tISAgpJDrmwOpLsrpnyCdD/lOZKvKkkJK4Dn0X5i7KF7THEZhOq+30bmhwBlNEaqPUiHiKtZg== integrity sha512-acWTYaha8xfhA/Du/z4sNZjHUWjkiuoAi2LM+T/aL+kemKQgPT1xBb/YKjlQ0Qo8gvbHsGNplrEJ+9G3gL7i4Q==
caniuse-lite@^1.0.30001629: caniuse-lite@^1.0.30001629:
version "1.0.30001636" version "1.0.30001636"
@ -10367,6 +10417,29 @@ next-seo@^6.1.0:
resolved "https://registry.npmjs.org/next-seo/-/next-seo-6.1.0.tgz" resolved "https://registry.npmjs.org/next-seo/-/next-seo-6.1.0.tgz"
integrity sha512-iMBpFoJsR5zWhguHJvsoBDxDSmdYTHtnVPB1ij+CD0NReQCP78ZxxbdL9qkKIf4oEuZEqZkrjAQLB0bkII7RYA== integrity sha512-iMBpFoJsR5zWhguHJvsoBDxDSmdYTHtnVPB1ij+CD0NReQCP78ZxxbdL9qkKIf4oEuZEqZkrjAQLB0bkII7RYA==
next@*:
version "14.1.0"
resolved "https://registry.yarnpkg.com/next/-/next-14.1.0.tgz#b31c0261ff9caa6b4a17c5af019ed77387174b69"
integrity sha512-wlzrsbfeSU48YQBjZhDzOwhWhGsy+uQycR8bHAOt1LY1bn3zZEcDyHQOEoN3aWzQ8LHCAJ1nqrWCc9XF2+O45Q==
dependencies:
"@next/env" "14.1.0"
"@swc/helpers" "0.5.2"
busboy "1.6.0"
caniuse-lite "^1.0.30001579"
graceful-fs "^4.2.11"
postcss "8.4.31"
styled-jsx "5.1.1"
optionalDependencies:
"@next/swc-darwin-arm64" "14.1.0"
"@next/swc-darwin-x64" "14.1.0"
"@next/swc-linux-arm64-gnu" "14.1.0"
"@next/swc-linux-arm64-musl" "14.1.0"
"@next/swc-linux-x64-gnu" "14.1.0"
"@next/swc-linux-x64-musl" "14.1.0"
"@next/swc-win32-arm64-msvc" "14.1.0"
"@next/swc-win32-ia32-msvc" "14.1.0"
"@next/swc-win32-x64-msvc" "14.1.0"
next@14.0.5-canary.46: next@14.0.5-canary.46:
version "14.0.5-canary.46" version "14.0.5-canary.46"
resolved "https://registry.yarnpkg.com/next/-/next-14.0.5-canary.46.tgz#003c8588488fd72bec70bcd4ea2f9ac65fd873f3" resolved "https://registry.yarnpkg.com/next/-/next-14.0.5-canary.46.tgz#003c8588488fd72bec70bcd4ea2f9ac65fd873f3"