💸 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
run: |
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
run: yarn turbo build:test --filter=@rallly/web

View file

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

View file

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

View file

@ -3,7 +3,6 @@
"pricingDescription": "Get started for free. No login required.",
"freeForever": "free forever",
"planPro": "Pro",
"annualBillingDescription": "per month, billed annually",
"monthlyBillingDescription": "per month",
"upgrade": "Upgrade",
"faq": "Frequently Asked Questions",
@ -28,5 +27,7 @@
"planFree": "Free",
"keepPollsIndefinitely": "Keep polls indefinitely",
"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 {
BillingPlan,
BillingPlanDescription,
@ -11,6 +14,7 @@ import {
import { Button } from "@rallly/ui/button";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@rallly/ui/tabs";
import { TrendingUpIcon } from "lucide-react";
import { GetStaticProps } from "next";
import Link from "next/link";
import { useTranslation } from "next-i18next";
import { NextSeo } from "next-seo";
@ -22,9 +26,6 @@ import { linkToApp } from "@/lib/linkToApp";
import { NextPageWithLayout } from "@/types";
import { getStaticTranslations } from "@/utils/page-translations";
const monthlyPriceUsd = 7;
const annualPriceUsd = 42;
export const UpgradeButton = ({
children,
annual,
@ -48,7 +49,7 @@ export const UpgradeButton = ({
);
};
const PriceTables = () => {
const PriceTables = ({ pricingData }: { pricingData: PricingData }) => {
const [tab, setTab] = React.useState("yearly");
return (
<Tabs value={tab} onValueChange={setTab}>
@ -57,8 +58,17 @@ const PriceTables = () => {
<TabsTrigger value="monthly">
<Trans i18nKey="pricing:billingPeriodMonthly" defaults="Monthly" />
</TabsTrigger>
<TabsTrigger value="yearly">
<TabsTrigger value="yearly" className="inline-flex gap-x-2.5">
<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>
</TabsList>
</div>
@ -115,18 +125,20 @@ const PriceTables = () => {
</BillingPlanDescription>
</BillingPlanHeader>
<TabsContent value="yearly">
<BillingPlanPrice discount={`$${(annualPriceUsd / 12).toFixed(2)}`}>
${monthlyPriceUsd}
<BillingPlanPrice>
${pricingData.yearly.amount / 100}
</BillingPlanPrice>
<BillingPlanPeriod>
<Trans
i18nKey="pricing:annualBillingDescription"
defaults="per month, billed annually"
i18nKey="pricing:yearlyBillingDescription"
defaults="per year"
/>
</BillingPlanPeriod>
</TabsContent>
<TabsContent value="monthly">
<BillingPlanPrice>${monthlyPriceUsd}</BillingPlanPrice>
<BillingPlanPrice>
${pricingData.monthly.amount / 100}
</BillingPlanPrice>
<BillingPlanPeriod>
<Trans
i18nKey="pricing:monthlyBillingDescription"
@ -266,7 +278,9 @@ const FAQ = () => {
);
};
const Page: NextPageWithLayout = () => {
const Page: NextPageWithLayout<{ pricingData: PricingData }> = ({
pricingData,
}) => {
const { t } = useTranslation(["pricing"]);
return (
<div className="mx-auto max-w-3xl">
@ -286,7 +300,7 @@ const Page: NextPageWithLayout = () => {
</p>
</div>
<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="mb-2">
<TrendingUpIcon className="text-indigo mr-2 mt-0.5 size-6 shrink-0" />
@ -317,4 +331,16 @@ Page.getLayout = getPageLayout;
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": {
"baseUrl": ".",
"paths": {
"@/*": ["src/*"],
"~/*": ["public/*"],
"@/*": [
"src/*"
],
"~/*": [
"public/*"
]
},
"checkJs": false,
"strictNullChecks": true
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
"exclude": ["node_modules"],
"include": [
"next-env.d.ts",
"**/*.ts",
"**/*.tsx",
".next/types/**/*.ts"
],
"exclude": [
"node_modules"
]
}

View file

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

View file

@ -139,7 +139,6 @@
"billingStatus": "Billing Status",
"billingStatusDescription": "Manage your subscription and billing details",
"freeForever": "free forever",
"annualBillingDescription": "per month, billed annually",
"billingStatusState": "Status",
"billingStatusActive": "Active",
"billingStatusPaused": "Paused",
@ -228,7 +227,6 @@
"autoTimeZoneHelp": "Enable this setting to automatically adjust event times to each participant's local time zone.",
"commentsDisabled": "Comments have been disabled",
"allParticipants": "All Participants",
"pollsListAll": "All",
"noParticipantsDescription": "Click <b>Share</b> to invite participants",
"timeShownIn": "Times shown in {timeZone}",
"pollStatusPausedDescription": "Votes cannot be submitted or edited at this time",
@ -263,5 +261,7 @@
"pastEventsEmptyStateTitle": "No Past Events",
"pastEventsEmptyStateDescription": "When you schedule events, they will appear here.",
"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 dayjs from "dayjs";
import { ArrowUpRight, CreditCardIcon, SendIcon } from "lucide-react";
import Head from "next/head";
import Link from "next/link";
import Script from "next/script";
import { useTranslation } from "next-i18next";
import { BillingPlans } from "@/components/billing/billing-plans";
import {
Settings,
SettingsContent,
@ -19,6 +16,8 @@ import { Trans } from "@/components/trans";
import { useSubscription } from "@/contexts/plan";
import { trpc } from "@/utils/trpc/client";
import { BillingPlans, PricingData } from "./billing-plans";
declare global {
interface Window {
// 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 SubscriptionStatus = () => {
const SubscriptionStatus = ({ pricingData }: { pricingData: PricingData }) => {
const data = useSubscription();
if (!data) {
@ -67,7 +66,7 @@ const SubscriptionStatus = () => {
return (
<div className="space-y-6">
{!data.active ? (
<BillingPlans />
<BillingPlans pricingData={pricingData} />
) : data.legacy ? (
<LegacyBilling />
) : (
@ -238,16 +237,11 @@ const LegacyBilling = () => {
</div>
</SettingsSection>;
export function BillingPage() {
const { t } = useTranslation();
export function BillingPage({ pricingData }: { pricingData: PricingData }) {
return (
<Settings>
<Head>
<title>{t("billing")}</title>
</Head>
<SettingsContent>
<SubscriptionStatus />
<SubscriptionStatus pricingData={pricingData} />
<hr />
<SettingsSection
title={<Trans i18nKey="support" defaults="Support" />}

View file

@ -1,3 +1,4 @@
import { Badge } from "@rallly/ui/badge";
import {
BillingPlan,
BillingPlanDescription,
@ -15,9 +16,21 @@ import React from "react";
import { Trans } from "@/components/trans";
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");
return (
@ -28,8 +41,21 @@ export const BillingPlans = () => {
<TabsTrigger value="monthly">
<Trans i18nKey="billingPeriodMonthly" />
</TabsTrigger>
<TabsTrigger value="yearly">
<TabsTrigger value="yearly" className="inline-flex gap-x-2.5">
<Trans i18nKey="billingPeriodYearly" />
<Badge variant="green">
<Trans
i18nKey="yearlyDiscount"
defaults="Save {amount}"
values={{
amount: `$${
(pricingData.monthly.amount * 12 -
pricingData.yearly.amount) /
100
}`,
}}
/>
</Badge>
</TabsTrigger>
</TabsList>
</div>
@ -85,20 +111,20 @@ export const BillingPlans = () => {
</div>
<div className="flex">
<TabsContent value="yearly">
<BillingPlanPrice
discount={`$${(annualPriceUsd / 12).toFixed(2)}`}
>
${monthlyPriceUsd}
<BillingPlanPrice>
${pricingData.yearly.amount / 100}
</BillingPlanPrice>
<BillingPlanPeriod>
<Trans
i18nKey="annualBillingDescription"
defaults="per month, billed annually"
i18nKey="yearlyBillingDescription"
defaults="per year"
/>
</BillingPlanPeriod>
</TabsContent>
<TabsContent value="monthly">
<BillingPlanPrice>${monthlyPriceUsd}</BillingPlanPrice>
<BillingPlanPrice>
${pricingData.monthly.amount / 100}
</BillingPlanPrice>
<BillingPlanPeriod>
<Trans
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 { getTranslation } from "@/app/i18n";
import { BillingPage } from "./billing-page";
import { env } from "@/env";
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 }) {

View file

@ -58,6 +58,7 @@ export const env = createEnv({
client: {
NEXT_PUBLIC_POSTHOG_API_KEY: z.string().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,
@ -88,6 +89,7 @@ export const env = createEnv({
ALLOWED_EMAILS: process.env.ALLOWED_EMAILS,
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_SELF_HOSTED: process.env.NEXT_PUBLIC_SELF_HOSTED,
},
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 { NextApiRequest, NextApiResponse } from "next";
import { z } from "zod";
@ -61,6 +61,8 @@ export default async function handler(
return;
}
const proPricingData = await getProPricing();
const session = await stripe.checkout.sessions.create({
success_url: absoluteUrl(
return_path ?? "/api/stripe/portal?session_id={CHECKOUT_SESSION_ID}",
@ -97,8 +99,8 @@ export default async function handler(
{
price:
period === "yearly"
? (process.env.STRIPE_YEARLY_PRICE as string)
: (process.env.STRIPE_MONTHLY_PRICE as string),
? proPricingData.yearly.id
: proPricingData.monthly.id,
quantity: 1,
},
],

View file

@ -8,7 +8,4 @@ export const isSelfHosted = process.env.NEXT_PUBLIC_SELF_HOSTED === "true";
export const isFeedbackEnabled = false;
export const monthlyPriceUsd = 7;
export const annualPriceUsd = 42;
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 = ({
children,
discount,
}: React.PropsWithChildren<{ discount?: React.ReactNode }>) => {
return (
<div>
{discount ? (
<>
<span className="mr-2 text-xl font-bold line-through">
{children}
</span>
<span className="text-3xl font-bold">{discount}</span>
</>
) : (
<div className="flex items-center gap-4">
<span className="text-3xl font-bold">{children}</span>
)}
</div>
);
};

View file

@ -2293,6 +2293,11 @@
resolved "https://registry.yarnpkg.com/@next/env/-/env-14.0.5-canary.46.tgz#b9b597baaba77a2836eaf836712a6e0afed1ca2d"
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":
version "14.2.4"
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"
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":
version "14.2.4"
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"
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":
version "14.2.4"
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"
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":
version "14.2.4"
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"
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":
version "14.2.4"
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"
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":
version "14.2.4"
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"
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":
version "14.2.4"
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"
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":
version "14.2.4"
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"
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":
version "14.2.4"
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"
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":
version "14.2.4"
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==
caniuse-lite@^1.0.30001579:
version "1.0.30001633"
resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001633.tgz#45a4ade9fb9ec80a06537a6271ac1e0afadcb324"
integrity sha512-6sT0yf/z5jqf8tISAgpJDrmwOpLsrpnyCdD/lOZKvKkkJK4Dn0X5i7KF7THEZhOq+30bmhwBlNEaqPUiHiKtZg==
version "1.0.30001583"
resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001583.tgz#abb2970cc370801dc7e27bf290509dc132cfa390"
integrity sha512-acWTYaha8xfhA/Du/z4sNZjHUWjkiuoAi2LM+T/aL+kemKQgPT1xBb/YKjlQ0Qo8gvbHsGNplrEJ+9G3gL7i4Q==
caniuse-lite@^1.0.30001629:
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"
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:
version "14.0.5-canary.46"
resolved "https://registry.yarnpkg.com/next/-/next-14.0.5-canary.46.tgz#003c8588488fd72bec70bcd4ea2f9ac65fd873f3"