mirror of
https://github.com/lukevella/rallly.git
synced 2025-04-29 10:16:32 +02:00
💸 Create abstraction for handling pricing (#1215)
This commit is contained in:
parent
299b33df62
commit
a5ee4fafe5
22 changed files with 302 additions and 68 deletions
1
.github/workflows/ci.yml
vendored
1
.github/workflows/ci.yml
vendored
|
@ -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
|
||||
|
|
1
apps/landing/next-env.d.ts
vendored
1
apps/landing/next-env.d.ts
vendored
|
@ -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.
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
};
|
||||
|
|
|
@ -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"
|
||||
]
|
||||
}
|
||||
|
|
|
@ -27,6 +27,7 @@
|
|||
"@rallly/backend": "*",
|
||||
"@rallly/database": "*",
|
||||
"@rallly/icons": "*",
|
||||
"@rallly/billing": "*",
|
||||
"@rallly/languages": "*",
|
||||
"@rallly/tailwind-config": "*",
|
||||
"@rallly/ui": "*",
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
|
|
|
@ -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" />}
|
||||
|
|
|
@ -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"
|
|
@ -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 }) {
|
||||
|
|
|
@ -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,
|
||||
});
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
],
|
||||
|
|
|
@ -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;
|
||||
|
|
16
packages/billing/package.json
Normal file
16
packages/billing/package.json
Normal 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": "*"
|
||||
}
|
||||
}
|
1
packages/billing/src/index.ts
Normal file
1
packages/billing/src/index.ts
Normal file
|
@ -0,0 +1 @@
|
|||
export * from "./lib/stripe";
|
20
packages/billing/src/lib/get-pricing.ts
Normal file
20
packages/billing/src/lib/get-pricing.ts
Normal 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,
|
||||
},
|
||||
};
|
||||
}
|
35
packages/billing/src/lib/stripe.ts
Normal file
35
packages/billing/src/lib/stripe.ts
Normal 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>>;
|
23
packages/billing/src/next/index.ts
Normal file
23
packages/billing/src/next/index.ts
Normal 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 };
|
5
packages/billing/tsconfig.json
Normal file
5
packages/billing/tsconfig.json
Normal file
|
@ -0,0 +1,5 @@
|
|||
{
|
||||
"extends": "@rallly/tsconfig/next.json",
|
||||
"include": ["**/*.ts", "**/*.tsx", "**/*.js"],
|
||||
"exclude": ["node_modules"],
|
||||
}
|
|
@ -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>
|
||||
</>
|
||||
) : (
|
||||
<span className="text-3xl font-bold">{children}</span>
|
||||
)}
|
||||
<div className="flex items-center gap-4">
|
||||
<span className="text-3xl font-bold">{children}</span>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
79
yarn.lock
79
yarn.lock
|
@ -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"
|
||||
|
|
Loading…
Add table
Reference in a new issue