mirror of
https://github.com/lukevella/rallly.git
synced 2025-05-10 15:36:49 +02:00
💳 Support payments with Stripe (#822)
This commit is contained in:
parent
969ae35971
commit
6f425edeaa
20 changed files with 712 additions and 229 deletions
|
@ -48,6 +48,7 @@
|
||||||
"iron-session": "^6.3.1",
|
"iron-session": "^6.3.1",
|
||||||
"js-cookie": "^3.0.1",
|
"js-cookie": "^3.0.1",
|
||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
|
"micro": "^10.0.1",
|
||||||
"nanoid": "^4.0.0",
|
"nanoid": "^4.0.0",
|
||||||
"next-i18next": "^13.0.3",
|
"next-i18next": "^13.0.3",
|
||||||
"next-seo": "^5.15.0",
|
"next-seo": "^5.15.0",
|
||||||
|
|
|
@ -211,8 +211,6 @@
|
||||||
"duplicateDescription": "Create a new poll based on this one",
|
"duplicateDescription": "Create a new poll based on this one",
|
||||||
"duplicateTitleLabel": "Title",
|
"duplicateTitleLabel": "Title",
|
||||||
"duplicateTitleDescription": "Hint: Give your new poll a unique title",
|
"duplicateTitleDescription": "Hint: Give your new poll a unique title",
|
||||||
"thankYou": "Thank you!",
|
|
||||||
"pleaseWait": "Your account is being upgraded. This should only take a few seconds.",
|
|
||||||
"proFeature": "Pro Feature",
|
"proFeature": "Pro Feature",
|
||||||
"upgradeOverlaySubtitle2": "Please upgrade to a paid plan to use this feature. This is how we keep the lights on :)",
|
"upgradeOverlaySubtitle2": "Please upgrade to a paid plan to use this feature. This is how we keep the lights on :)",
|
||||||
"savePercent": "Save {percent}%",
|
"savePercent": "Save {percent}%",
|
||||||
|
@ -227,5 +225,10 @@
|
||||||
"scrollLeft": "Scroll Left",
|
"scrollLeft": "Scroll Left",
|
||||||
"scrollRight": "Scroll Right",
|
"scrollRight": "Scroll Right",
|
||||||
"shrink": "Shrink",
|
"shrink": "Shrink",
|
||||||
"expand": "Expand"
|
"expand": "Expand",
|
||||||
|
"activeSubscription": "Thank you for subscribing to Rallly Pro. You can manage your subscription and billing details from the billing portal.",
|
||||||
|
"billingPortal": "Billing Portal",
|
||||||
|
"supportDescription": "Need help with anything?",
|
||||||
|
"supportBilling": "Please reach out if you need any assistance.",
|
||||||
|
"contactSupport": "Contact Support"
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,11 +1,9 @@
|
||||||
import { trpc } from "@rallly/backend";
|
|
||||||
import {
|
import {
|
||||||
CalendarCheck2Icon,
|
CalendarCheck2Icon,
|
||||||
CopyIcon,
|
CopyIcon,
|
||||||
DatabaseIcon,
|
DatabaseIcon,
|
||||||
HeartIcon,
|
HeartIcon,
|
||||||
ImageOffIcon,
|
ImageOffIcon,
|
||||||
Loader2Icon,
|
|
||||||
LockIcon,
|
LockIcon,
|
||||||
Settings2Icon,
|
Settings2Icon,
|
||||||
TrendingUpIcon,
|
TrendingUpIcon,
|
||||||
|
@ -51,32 +49,8 @@ const Feature = ({
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const ThankYou = () => {
|
|
||||||
trpc.user.getBilling.useQuery(undefined, {
|
|
||||||
refetchInterval: 1000,
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="space-y-2 text-center">
|
|
||||||
<h2>
|
|
||||||
<Trans i18nKey="thankYou" defaults="Thank you!" />
|
|
||||||
</h2>
|
|
||||||
<p className="text-muted-foreground mx-auto max-w-xs text-sm">
|
|
||||||
<Trans
|
|
||||||
i18nKey="pleaseWait"
|
|
||||||
defaults="Your account is being upgraded. This should only take a few seconds."
|
|
||||||
/>
|
|
||||||
</p>
|
|
||||||
<div className="p-4 text-gray-500">
|
|
||||||
<Loader2Icon className="inline-block h-7 w-7 animate-spin" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const Teaser = () => {
|
const Teaser = () => {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const [didUpgrade, setDidUpgrade] = React.useState(false);
|
|
||||||
|
|
||||||
const [tab, setTab] = React.useState("yearly");
|
const [tab, setTab] = React.useState("yearly");
|
||||||
|
|
||||||
|
@ -106,140 +80,132 @@ const Teaser = () => {
|
||||||
className="text-center"
|
className="text-center"
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
>
|
>
|
||||||
<Badge className="translate-y-0 py-0.5 px-4 text-lg">
|
<Badge className="translate-y-0 px-4 py-0.5 text-lg">
|
||||||
<Trans i18nKey="planPro" />
|
<Trans i18nKey="planPro" />
|
||||||
</Badge>
|
</Badge>
|
||||||
</m.div>
|
</m.div>
|
||||||
</div>
|
</div>
|
||||||
{didUpgrade ? (
|
|
||||||
<ThankYou />
|
<div className="space-y-6">
|
||||||
) : (
|
<div className="space-y-2 text-center">
|
||||||
<div className="space-y-6">
|
<h2 className="text-center">
|
||||||
<div className="space-y-2 text-center">
|
<Trans defaults="Pro Feature" i18nKey="proFeature" />
|
||||||
<h2 className="text-center">
|
</h2>
|
||||||
<Trans defaults="Pro Feature" i18nKey="proFeature" />
|
<p className="text-muted-foreground mx-auto max-w-xs text-center text-sm leading-relaxed">
|
||||||
</h2>
|
|
||||||
<p className="text-muted-foreground mx-auto max-w-xs text-center text-sm leading-relaxed">
|
|
||||||
<Trans
|
|
||||||
i18nKey="upgradeOverlaySubtitle2"
|
|
||||||
defaults="Please upgrade to a paid plan to use this feature. This is how we keep the lights on :)"
|
|
||||||
/>
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<Tabs
|
|
||||||
className="flex flex-col items-center gap-4"
|
|
||||||
value={tab}
|
|
||||||
onValueChange={setTab}
|
|
||||||
>
|
|
||||||
<TabsList>
|
|
||||||
<TabsTrigger value="monthly">
|
|
||||||
<Trans i18nKey="billingPeriodMonthly" />
|
|
||||||
</TabsTrigger>
|
|
||||||
<TabsTrigger value="yearly">
|
|
||||||
<Trans i18nKey="billingPeriodYearly" />
|
|
||||||
</TabsTrigger>
|
|
||||||
</TabsList>
|
|
||||||
<TabsContent value="monthly">
|
|
||||||
<div>
|
|
||||||
<div className="flex items-start justify-center gap-2.5">
|
|
||||||
<div className=" text-4xl font-bold">$5</div>
|
|
||||||
<div>
|
|
||||||
<div className="text-xs font-semibold leading-5">USD</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="text-muted-foreground text-sm">
|
|
||||||
<Trans i18nKey="monthlyBillingDescription" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</TabsContent>
|
|
||||||
<TabsContent value="yearly">
|
|
||||||
<div className="text-center">
|
|
||||||
<div className="flex items-start justify-center gap-2.5">
|
|
||||||
<div className="flex items-end gap-2">
|
|
||||||
<div className="font-bold text-gray-500 line-through">
|
|
||||||
$5
|
|
||||||
</div>
|
|
||||||
<div className=" text-4xl font-bold">$2.50</div>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<div className="mt-1 text-xs font-semibold">USD</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="text-muted-foreground text-sm">
|
|
||||||
<Trans i18nKey="annualBillingDescription" />
|
|
||||||
</div>
|
|
||||||
<p className="mt-2">
|
|
||||||
<span className="rounded border border-dashed border-green-400 px-1 py-0.5 text-xs text-green-500">
|
|
||||||
<Trans
|
|
||||||
i18nKey="savePercent"
|
|
||||||
defaults="Save {percent}%"
|
|
||||||
values={{ percent: 50 }}
|
|
||||||
/>
|
|
||||||
</span>
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</TabsContent>
|
|
||||||
</Tabs>
|
|
||||||
<div className="space-y-2">
|
|
||||||
<p className="text-primary text-center text-xs">
|
|
||||||
<TrendingUpIcon className="mr-2 inline-block h-4 w-4" />
|
|
||||||
<Trans
|
|
||||||
i18nKey="priceIncreaseSoon"
|
|
||||||
defaults="Price increase soon."
|
|
||||||
/>
|
|
||||||
</p>
|
|
||||||
<p className="text-center text-xs text-gray-400">
|
|
||||||
<LockIcon className="mr-2 inline-block h-4 w-4" />
|
|
||||||
<Trans
|
|
||||||
i18nKey="lockPrice"
|
|
||||||
defaults="Upgrade today to keep this price forever."
|
|
||||||
/>
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<h3 className="mx-auto max-w-sm text-center">
|
|
||||||
<Trans
|
<Trans
|
||||||
i18nKey="features"
|
i18nKey="upgradeOverlaySubtitle2"
|
||||||
defaults="Get access to all current and future Pro features!"
|
defaults="Please upgrade to a paid plan to use this feature. This is how we keep the lights on :)"
|
||||||
/>
|
/>
|
||||||
</h3>
|
</p>
|
||||||
<ul className="flex flex-wrap justify-center gap-2 border-gray-100 bg-[radial-gradient(ellipse_at_center,_var(--tw-gradient-stops))] from-gray-100 via-transparent">
|
|
||||||
<Feature className="bg-violet-500" icon={ImageOffIcon}>
|
|
||||||
<Trans i18nKey="noAds" defaults="No ads" />
|
|
||||||
</Feature>
|
|
||||||
<Feature className="bg-rose-500" icon={DatabaseIcon}>
|
|
||||||
<Trans
|
|
||||||
i18nKey="plan_extendedPollLife"
|
|
||||||
defaults="Extend poll life"
|
|
||||||
/>
|
|
||||||
</Feature>
|
|
||||||
<Feature className="bg-green-500" icon={CalendarCheck2Icon}>
|
|
||||||
<Trans i18nKey="finalize" defaults="Finalize" />
|
|
||||||
</Feature>
|
|
||||||
<Feature className="bg-teal-500" icon={CopyIcon}>
|
|
||||||
<Trans i18nKey="duplicate" defaults="Duplicate" />
|
|
||||||
</Feature>
|
|
||||||
<Feature className="bg-gray-700" icon={Settings2Icon}>
|
|
||||||
<Trans i18nKey="settings" defaults="Settings" />
|
|
||||||
</Feature>
|
|
||||||
<Feature className="bg-pink-600" icon={HeartIcon}>
|
|
||||||
<Trans i18nKey="supportProject" defaults="Support this project" />
|
|
||||||
</Feature>
|
|
||||||
</ul>
|
|
||||||
<div className="grid gap-2.5">
|
|
||||||
<UpgradeButton
|
|
||||||
annual={tab === "yearly"}
|
|
||||||
onUpgrade={() => setDidUpgrade(true)}
|
|
||||||
>
|
|
||||||
<Trans i18nKey="upgrade" defaults="Upgrade" />
|
|
||||||
</UpgradeButton>
|
|
||||||
<Button asChild className="w-full">
|
|
||||||
<Link href={`/poll/${router.query.urlId as string}`}>
|
|
||||||
<Trans i18nKey="notToday" defaults="Not Today" />
|
|
||||||
</Link>
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
<Tabs
|
||||||
|
className="flex flex-col items-center gap-4"
|
||||||
|
value={tab}
|
||||||
|
onValueChange={setTab}
|
||||||
|
>
|
||||||
|
<TabsList>
|
||||||
|
<TabsTrigger value="monthly">
|
||||||
|
<Trans i18nKey="billingPeriodMonthly" />
|
||||||
|
</TabsTrigger>
|
||||||
|
<TabsTrigger value="yearly">
|
||||||
|
<Trans i18nKey="billingPeriodYearly" />
|
||||||
|
</TabsTrigger>
|
||||||
|
</TabsList>
|
||||||
|
<TabsContent value="monthly">
|
||||||
|
<div>
|
||||||
|
<div className="flex items-start justify-center gap-2.5">
|
||||||
|
<div className=" text-4xl font-bold">$5</div>
|
||||||
|
<div>
|
||||||
|
<div className="text-xs font-semibold leading-5">USD</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="text-muted-foreground text-sm">
|
||||||
|
<Trans i18nKey="monthlyBillingDescription" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</TabsContent>
|
||||||
|
<TabsContent value="yearly">
|
||||||
|
<div className="text-center">
|
||||||
|
<div className="flex items-start justify-center gap-2.5">
|
||||||
|
<div className="flex items-end gap-2">
|
||||||
|
<div className="font-bold text-gray-500 line-through">$5</div>
|
||||||
|
<div className=" text-4xl font-bold">$2.50</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div className="mt-1 text-xs font-semibold">USD</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="text-muted-foreground text-sm">
|
||||||
|
<Trans i18nKey="annualBillingDescription" />
|
||||||
|
</div>
|
||||||
|
<p className="mt-2">
|
||||||
|
<span className="rounded border border-dashed border-green-400 px-1 py-0.5 text-xs text-green-500">
|
||||||
|
<Trans
|
||||||
|
i18nKey="savePercent"
|
||||||
|
defaults="Save {percent}%"
|
||||||
|
values={{ percent: 50 }}
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</TabsContent>
|
||||||
|
</Tabs>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<p className="text-primary text-center text-xs">
|
||||||
|
<TrendingUpIcon className="mr-2 inline-block h-4 w-4" />
|
||||||
|
<Trans
|
||||||
|
i18nKey="priceIncreaseSoon"
|
||||||
|
defaults="Price increase soon."
|
||||||
|
/>
|
||||||
|
</p>
|
||||||
|
<p className="text-center text-xs text-gray-400">
|
||||||
|
<LockIcon className="mr-2 inline-block h-4 w-4" />
|
||||||
|
<Trans
|
||||||
|
i18nKey="lockPrice"
|
||||||
|
defaults="Upgrade today to keep this price forever."
|
||||||
|
/>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<h3 className="mx-auto max-w-sm text-center">
|
||||||
|
<Trans
|
||||||
|
i18nKey="features"
|
||||||
|
defaults="Get access to all current and future Pro features!"
|
||||||
|
/>
|
||||||
|
</h3>
|
||||||
|
<ul className="flex flex-wrap justify-center gap-2 border-gray-100 bg-[radial-gradient(ellipse_at_center,_var(--tw-gradient-stops))] from-gray-100 via-transparent">
|
||||||
|
<Feature className="bg-violet-500" icon={ImageOffIcon}>
|
||||||
|
<Trans i18nKey="noAds" defaults="No ads" />
|
||||||
|
</Feature>
|
||||||
|
<Feature className="bg-rose-500" icon={DatabaseIcon}>
|
||||||
|
<Trans
|
||||||
|
i18nKey="plan_extendedPollLife"
|
||||||
|
defaults="Extend poll life"
|
||||||
|
/>
|
||||||
|
</Feature>
|
||||||
|
<Feature className="bg-green-500" icon={CalendarCheck2Icon}>
|
||||||
|
<Trans i18nKey="finalize" defaults="Finalize" />
|
||||||
|
</Feature>
|
||||||
|
<Feature className="bg-teal-500" icon={CopyIcon}>
|
||||||
|
<Trans i18nKey="duplicate" defaults="Duplicate" />
|
||||||
|
</Feature>
|
||||||
|
<Feature className="bg-gray-700" icon={Settings2Icon}>
|
||||||
|
<Trans i18nKey="settings" defaults="Settings" />
|
||||||
|
</Feature>
|
||||||
|
<Feature className="bg-pink-600" icon={HeartIcon}>
|
||||||
|
<Trans i18nKey="supportProject" defaults="Support this project" />
|
||||||
|
</Feature>
|
||||||
|
</ul>
|
||||||
|
<div className="grid gap-2.5">
|
||||||
|
<UpgradeButton annual={tab === "yearly"}>
|
||||||
|
<Trans i18nKey="upgrade" defaults="Upgrade" />
|
||||||
|
</UpgradeButton>
|
||||||
|
<Button asChild className="w-full">
|
||||||
|
<Link href={`/poll/${router.query.urlId as string}`}>
|
||||||
|
<Trans i18nKey="notToday" defaults="Not Today" />
|
||||||
|
</Link>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</m.div>
|
</m.div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,56 +1,32 @@
|
||||||
import { trpc } from "@rallly/backend";
|
|
||||||
import { Button } from "@rallly/ui/button";
|
import { Button } from "@rallly/ui/button";
|
||||||
import { useRouter } from "next/router";
|
import Link from "next/link";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
|
|
||||||
import { useUser } from "@/components/user-provider";
|
|
||||||
import { planIdMonthly, planIdYearly } from "@/utils/constants";
|
|
||||||
import { usePostHog } from "@/utils/posthog";
|
import { usePostHog } from "@/utils/posthog";
|
||||||
|
|
||||||
export const UpgradeButton = ({
|
export const UpgradeButton = ({
|
||||||
children,
|
children,
|
||||||
onUpgrade,
|
|
||||||
annual,
|
annual,
|
||||||
}: React.PropsWithChildren<{ annual?: boolean; onUpgrade?: () => void }>) => {
|
}: React.PropsWithChildren<{ annual?: boolean }>) => {
|
||||||
const posthog = usePostHog();
|
const posthog = usePostHog();
|
||||||
const [isPendingSubscription, setPendingSubscription] = React.useState(false);
|
|
||||||
const { user } = useUser();
|
|
||||||
const router = useRouter();
|
|
||||||
|
|
||||||
trpc.user.getBilling.useQuery(undefined, {
|
|
||||||
refetchInterval: isPendingSubscription ? 1000 : 0,
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Button
|
<Button
|
||||||
className="w-full"
|
className="w-full"
|
||||||
loading={isPendingSubscription}
|
|
||||||
variant="primary"
|
variant="primary"
|
||||||
|
asChild
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
posthog?.capture("click upgrade button");
|
posthog?.capture("click upgrade button");
|
||||||
if (user.isGuest) {
|
|
||||||
router.push("/login");
|
|
||||||
} else {
|
|
||||||
window.Paddle.Checkout.open({
|
|
||||||
allowQuantity: false,
|
|
||||||
product: annual ? planIdYearly : planIdMonthly,
|
|
||||||
email: user.email,
|
|
||||||
disableLogout: true,
|
|
||||||
passthrough: JSON.stringify({ userId: user.id }),
|
|
||||||
successCallback: () => {
|
|
||||||
posthog?.capture("upgrade", {
|
|
||||||
period: annual ? "yearly" : "monthly",
|
|
||||||
});
|
|
||||||
onUpgrade?.();
|
|
||||||
// fetch user till we get the new plan
|
|
||||||
setPendingSubscription(true);
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{children}
|
<Link
|
||||||
|
href={`/api/stripe/checkout?period=${
|
||||||
|
annual ? "yearly" : "monthly"
|
||||||
|
}&return_path=${encodeURIComponent(window.location.pathname)}`}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</Link>
|
||||||
</Button>
|
</Button>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|
|
@ -1,4 +1,3 @@
|
||||||
import { trpc } from "@rallly/backend";
|
|
||||||
import {
|
import {
|
||||||
ChevronDown,
|
ChevronDown,
|
||||||
CreditCardIcon,
|
CreditCardIcon,
|
||||||
|
@ -26,19 +25,15 @@ import Link from "next/link";
|
||||||
|
|
||||||
import { Trans } from "@/components/trans";
|
import { Trans } from "@/components/trans";
|
||||||
import { CurrentUserAvatar } from "@/components/user";
|
import { CurrentUserAvatar } from "@/components/user";
|
||||||
|
import { usePlan } from "@/contexts/plan";
|
||||||
import { isFeedbackEnabled } from "@/utils/constants";
|
import { isFeedbackEnabled } from "@/utils/constants";
|
||||||
|
|
||||||
import { IfAuthenticated, IfGuest, useUser } from "./user-provider";
|
import { IfAuthenticated, IfGuest, useUser } from "./user-provider";
|
||||||
|
|
||||||
const Plan = () => {
|
const Plan = () => {
|
||||||
const { isFetched, data } = trpc.user.getBilling.useQuery();
|
const plan = usePlan();
|
||||||
if (!isFetched) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const isPlus = data && data.endDate.getTime() > Date.now();
|
if (plan === "paid") {
|
||||||
|
|
||||||
if (isPlus) {
|
|
||||||
return (
|
return (
|
||||||
<Badge>
|
<Badge>
|
||||||
<Trans i18nKey="planPro" defaults="Pro" />
|
<Trans i18nKey="planPro" defaults="Pro" />
|
||||||
|
|
|
@ -50,7 +50,7 @@ export const UserProvider = (props: { children?: React.ReactNode }) => {
|
||||||
const queryClient = trpc.useContext();
|
const queryClient = trpc.useContext();
|
||||||
|
|
||||||
const user = useWhoAmI();
|
const user = useWhoAmI();
|
||||||
const billingQuery = trpc.user.getBilling.useQuery();
|
const subscriptionQuery = trpc.user.subscription.useQuery();
|
||||||
const { data: userPreferences } = trpc.userPreferences.get.useQuery();
|
const { data: userPreferences } = trpc.userPreferences.get.useQuery();
|
||||||
|
|
||||||
const shortName = user
|
const shortName = user
|
||||||
|
@ -59,7 +59,7 @@ export const UserProvider = (props: { children?: React.ReactNode }) => {
|
||||||
: user.id.substring(0, 10)
|
: user.id.substring(0, 10)
|
||||||
: t("guest");
|
: t("guest");
|
||||||
|
|
||||||
if (!user || userPreferences === undefined || !billingQuery.isFetched) {
|
if (!user || userPreferences === undefined || !subscriptionQuery.isFetched) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,11 +1,9 @@
|
||||||
import { trpc } from "@rallly/backend";
|
import { trpc } from "@rallly/backend";
|
||||||
|
|
||||||
export const usePlan = () => {
|
export const usePlan = () => {
|
||||||
const { data } = trpc.user.getBilling.useQuery(undefined, {
|
const { data } = trpc.user.subscription.useQuery();
|
||||||
staleTime: 10 * 1000,
|
|
||||||
});
|
|
||||||
|
|
||||||
const isPaid = Boolean(data && data.endDate.getTime() > Date.now());
|
const isPaid = data?.active === true;
|
||||||
|
|
||||||
return isPaid ? "paid" : "free";
|
return isPaid ? "paid" : "free";
|
||||||
};
|
};
|
||||||
|
|
105
apps/web/src/pages/api/stripe/checkout.ts
Normal file
105
apps/web/src/pages/api/stripe/checkout.ts
Normal file
|
@ -0,0 +1,105 @@
|
||||||
|
import { getSession } from "@rallly/backend/next/session";
|
||||||
|
import { stripe } from "@rallly/backend/stripe";
|
||||||
|
import { prisma } from "@rallly/database";
|
||||||
|
import { absoluteUrl } from "@rallly/utils";
|
||||||
|
import { NextApiRequest, NextApiResponse } from "next";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
export const config = {
|
||||||
|
edge: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
const inputSchema = z.object({
|
||||||
|
period: z.enum(["monthly", "yearly"]).optional(),
|
||||||
|
success_path: z.string().optional(),
|
||||||
|
return_path: z.string().optional(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export default async function handler(
|
||||||
|
req: NextApiRequest,
|
||||||
|
res: NextApiResponse,
|
||||||
|
) {
|
||||||
|
const userSession = await getSession(req, res);
|
||||||
|
|
||||||
|
if (userSession.user?.isGuest !== false) {
|
||||||
|
// You need to be logged in to subscribe
|
||||||
|
return res
|
||||||
|
.status(403)
|
||||||
|
.redirect(
|
||||||
|
`/login${req.url ? `?redirect=${encodeURIComponent(req.url)}` : ""}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const { period = "monthly", return_path } = inputSchema.parse(req.query);
|
||||||
|
|
||||||
|
const user = await prisma.user.findUnique({
|
||||||
|
where: {
|
||||||
|
id: userSession.user.id,
|
||||||
|
},
|
||||||
|
select: {
|
||||||
|
email: true,
|
||||||
|
customerId: true,
|
||||||
|
subscription: {
|
||||||
|
select: {
|
||||||
|
active: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!user) {
|
||||||
|
return res.status(403).redirect("/logout");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (user.subscription?.active === true) {
|
||||||
|
// User already has an active subscription. Take them to customer portal
|
||||||
|
return res.redirect("/api/stripe/portal");
|
||||||
|
}
|
||||||
|
|
||||||
|
const session = await stripe.checkout.sessions.create({
|
||||||
|
success_url: absoluteUrl(
|
||||||
|
return_path ?? "/api/stripe/portal?session_id={CHECKOUT_SESSION_ID}",
|
||||||
|
),
|
||||||
|
cancel_url: absoluteUrl(return_path),
|
||||||
|
...(user.customerId
|
||||||
|
? {
|
||||||
|
// use existing customer if available to reuse payment details
|
||||||
|
customer: user.customerId,
|
||||||
|
customer_update: {
|
||||||
|
// needed for tax id collection
|
||||||
|
name: "auto",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
: {
|
||||||
|
// supply email if user is not a customer yet
|
||||||
|
customer_email: user.email,
|
||||||
|
}),
|
||||||
|
mode: "subscription",
|
||||||
|
allow_promotion_codes: true,
|
||||||
|
billing_address_collection: "auto",
|
||||||
|
tax_id_collection: {
|
||||||
|
enabled: true,
|
||||||
|
},
|
||||||
|
metadata: {
|
||||||
|
userId: userSession.user.id,
|
||||||
|
},
|
||||||
|
line_items: [
|
||||||
|
{
|
||||||
|
price:
|
||||||
|
period === "yearly"
|
||||||
|
? (process.env.STRIPE_YEARLY_PRICE as string)
|
||||||
|
: (process.env.STRIPE_MONTHLY_PRICE as string),
|
||||||
|
quantity: 1,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
if (session.url) {
|
||||||
|
// redirect to checkout session
|
||||||
|
return res.status(303).redirect(session.url);
|
||||||
|
}
|
||||||
|
|
||||||
|
return res
|
||||||
|
.status(500)
|
||||||
|
.json({ error: "Something went wrong while creating a checkout session" });
|
||||||
|
}
|
58
apps/web/src/pages/api/stripe/portal.ts
Normal file
58
apps/web/src/pages/api/stripe/portal.ts
Normal file
|
@ -0,0 +1,58 @@
|
||||||
|
import { getSession } from "@rallly/backend/next/session";
|
||||||
|
import { stripe } from "@rallly/backend/stripe";
|
||||||
|
import { absoluteUrl } from "@rallly/utils";
|
||||||
|
import { NextApiRequest, NextApiResponse } from "next";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
const inputSchema = z.object({
|
||||||
|
session_id: z.string().optional(),
|
||||||
|
return_path: z.string().optional(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export default async function handler(
|
||||||
|
req: NextApiRequest,
|
||||||
|
res: NextApiResponse,
|
||||||
|
) {
|
||||||
|
const userSession = await getSession(req, res);
|
||||||
|
|
||||||
|
if (userSession.user?.isGuest !== false) {
|
||||||
|
// You need to be logged in to subscribe
|
||||||
|
return res
|
||||||
|
.status(403)
|
||||||
|
.redirect(
|
||||||
|
`/login${req.url ? `?redirect=${encodeURIComponent(req.url)}` : ""}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const user = await prisma?.user.findUnique({
|
||||||
|
where: {
|
||||||
|
id: userSession.user.id,
|
||||||
|
},
|
||||||
|
select: {
|
||||||
|
email: true,
|
||||||
|
customerId: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!user) {
|
||||||
|
return res.status(403).redirect("/logout");
|
||||||
|
}
|
||||||
|
|
||||||
|
const { session_id: sessionId, return_path } = inputSchema.parse(req.query);
|
||||||
|
|
||||||
|
let customerId: string;
|
||||||
|
|
||||||
|
if (sessionId) {
|
||||||
|
const session = await stripe.checkout.sessions.retrieve(sessionId);
|
||||||
|
customerId = session.customer as string;
|
||||||
|
} else {
|
||||||
|
customerId = user.customerId as string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const portalSession = await stripe.billingPortal.sessions.create({
|
||||||
|
customer: customerId,
|
||||||
|
return_url: absoluteUrl(return_path),
|
||||||
|
});
|
||||||
|
|
||||||
|
res.status(303).redirect(portalSession.url);
|
||||||
|
}
|
110
apps/web/src/pages/api/stripe/webhook.ts
Normal file
110
apps/web/src/pages/api/stripe/webhook.ts
Normal file
|
@ -0,0 +1,110 @@
|
||||||
|
import type { Stripe } from "@rallly/backend/stripe";
|
||||||
|
import { stripe } from "@rallly/backend/stripe";
|
||||||
|
import { prisma } from "@rallly/database";
|
||||||
|
import { buffer } from "micro";
|
||||||
|
import { NextApiRequest, NextApiResponse } from "next";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
export const config = {
|
||||||
|
api: {
|
||||||
|
bodyParser: false,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const toDate = (date: number) => new Date(date * 1000);
|
||||||
|
|
||||||
|
const endpointSecret = process.env.STRIPE_SIGNING_SECRET as string;
|
||||||
|
|
||||||
|
const validatedWebhook = async (req: NextApiRequest) => {
|
||||||
|
const signature = req.headers["stripe-signature"] as string;
|
||||||
|
const buf = await buffer(req);
|
||||||
|
|
||||||
|
try {
|
||||||
|
return stripe.webhooks.constructEvent(buf, signature, endpointSecret);
|
||||||
|
} catch (err) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const metadataSchema = z.object({
|
||||||
|
userId: z.string(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export default async function handler(
|
||||||
|
req: NextApiRequest,
|
||||||
|
res: NextApiResponse,
|
||||||
|
) {
|
||||||
|
if (req.method !== "POST") {
|
||||||
|
return res.status(405).end();
|
||||||
|
}
|
||||||
|
if (!endpointSecret) {
|
||||||
|
return res.status(400).send("No endpoint secret");
|
||||||
|
}
|
||||||
|
const event = await validatedWebhook(req);
|
||||||
|
|
||||||
|
if (!event) {
|
||||||
|
return res.status(400).send("Invalid signature");
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (event.type) {
|
||||||
|
case "checkout.session.completed":
|
||||||
|
const checkoutSession = event.data.object as Stripe.Checkout.Session;
|
||||||
|
const { userId } = metadataSchema.parse(checkoutSession.metadata);
|
||||||
|
|
||||||
|
if (!userId) {
|
||||||
|
return res.status(400).send("Missing client reference ID");
|
||||||
|
}
|
||||||
|
|
||||||
|
await prisma.user.update({
|
||||||
|
where: {
|
||||||
|
id: userId,
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
customerId: checkoutSession.customer as string,
|
||||||
|
subscriptionId: checkoutSession.subscription as string,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
case "customer.subscription.deleted":
|
||||||
|
case "customer.subscription.updated":
|
||||||
|
case "customer.subscription.created":
|
||||||
|
const subscription = event.data.object as Stripe.Subscription;
|
||||||
|
|
||||||
|
// check if the subscription is active
|
||||||
|
const isActive = subscription.status === "active";
|
||||||
|
|
||||||
|
// get the subscription price details
|
||||||
|
const lineItem = subscription.items.data[0];
|
||||||
|
|
||||||
|
// update/create the subscription in the database
|
||||||
|
const { price } = lineItem;
|
||||||
|
await prisma.subscription.upsert({
|
||||||
|
where: {
|
||||||
|
id: subscription.id,
|
||||||
|
},
|
||||||
|
create: {
|
||||||
|
id: subscription.id,
|
||||||
|
active: isActive,
|
||||||
|
priceId: price.id,
|
||||||
|
currency: price.currency ?? null,
|
||||||
|
createdAt: toDate(subscription.created),
|
||||||
|
periodStart: toDate(subscription.current_period_start),
|
||||||
|
periodEnd: toDate(subscription.current_period_end),
|
||||||
|
},
|
||||||
|
update: {
|
||||||
|
active: isActive,
|
||||||
|
priceId: price.id,
|
||||||
|
currency: price.currency ?? null,
|
||||||
|
createdAt: toDate(subscription.created),
|
||||||
|
periodStart: toDate(subscription.current_period_start),
|
||||||
|
periodEnd: toDate(subscription.current_period_end),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
// Unexpected event type
|
||||||
|
console.error(`Unhandled event type ${event.type}.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
res.end();
|
||||||
|
}
|
|
@ -1,5 +1,6 @@
|
||||||
import { trpc } from "@rallly/backend";
|
import { trpc } from "@rallly/backend";
|
||||||
import { CreditCardIcon } from "@rallly/icons";
|
import { ArrowUpRight, CreditCardIcon } from "@rallly/icons";
|
||||||
|
import { Badge } from "@rallly/ui/badge";
|
||||||
import { Button } from "@rallly/ui/button";
|
import { Button } from "@rallly/ui/button";
|
||||||
import { Card } from "@rallly/ui/card";
|
import { Card } from "@rallly/ui/card";
|
||||||
import { Label } from "@rallly/ui/label";
|
import { Label } from "@rallly/ui/label";
|
||||||
|
@ -13,7 +14,6 @@ import { getProfileLayout } from "@/components/layouts/profile-layout";
|
||||||
import { SettingsSection } from "@/components/settings/settings-section";
|
import { SettingsSection } from "@/components/settings/settings-section";
|
||||||
import { Trans } from "@/components/trans";
|
import { Trans } from "@/components/trans";
|
||||||
import { useUser } from "@/components/user-provider";
|
import { useUser } from "@/components/user-provider";
|
||||||
import { usePlan } from "@/contexts/plan";
|
|
||||||
|
|
||||||
import { NextPageWithLayout } from "../../types";
|
import { NextPageWithLayout } from "../../types";
|
||||||
import { getStaticTranslations } from "../../utils/with-page-translations";
|
import { getStaticTranslations } from "../../utils/with-page-translations";
|
||||||
|
@ -25,6 +25,38 @@ declare global {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const BillingPortal = () => {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div className="mb-4 flex items-center justify-between">
|
||||||
|
<Badge>
|
||||||
|
<Trans i18nKey="planPro" />
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
<p>
|
||||||
|
<Trans
|
||||||
|
i18nKey="activeSubscription"
|
||||||
|
defaults="Thank you for subscribing to Rallly Pro. You can manage your subscription and billing details from the billing portal."
|
||||||
|
/>
|
||||||
|
</p>
|
||||||
|
<div className="mt-6">
|
||||||
|
<Button asChild>
|
||||||
|
<Link
|
||||||
|
href={`/api/stripe/portal?return_path=${encodeURIComponent(
|
||||||
|
window.location.pathname,
|
||||||
|
)}`}
|
||||||
|
>
|
||||||
|
<span>
|
||||||
|
<Trans i18nKey="billingPortal" defaults="Billing Portal" />
|
||||||
|
</span>
|
||||||
|
<ArrowUpRight className="h-4 w-4" />
|
||||||
|
</Link>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
export const proPlanIdMonthly = process.env
|
export const proPlanIdMonthly = process.env
|
||||||
.NEXT_PUBLIC_PRO_PLAN_ID_MONTHLY as string;
|
.NEXT_PUBLIC_PRO_PLAN_ID_MONTHLY as string;
|
||||||
|
|
||||||
|
@ -34,25 +66,31 @@ export const proPlanIdYearly = process.env
|
||||||
const SubscriptionStatus = () => {
|
const SubscriptionStatus = () => {
|
||||||
const { user } = useUser();
|
const { user } = useUser();
|
||||||
|
|
||||||
const plan = usePlan();
|
const { data } = trpc.user.subscription.useQuery();
|
||||||
const isPlus = plan === "paid";
|
|
||||||
|
if (!data) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
if (user.isGuest) {
|
if (user.isGuest) {
|
||||||
return <>You need to be logged in.</>;
|
return <>You need to be logged in.</>;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isPlus) {
|
if (data.legacy) {
|
||||||
return <BillingStatus />;
|
// User is on the old billing system
|
||||||
|
return <LegacyBilling />;
|
||||||
|
} else if (data.active) {
|
||||||
|
return <BillingPortal />;
|
||||||
} else {
|
} else {
|
||||||
return <BillingPlans />;
|
return <BillingPlans />;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const BillingStatus = () => {
|
const LegacyBilling = () => {
|
||||||
const { data: userPaymentData } = trpc.user.getBilling.useQuery();
|
const { data: userPaymentData } = trpc.user.getBilling.useQuery();
|
||||||
|
|
||||||
if (!userPaymentData) {
|
if (!userPaymentData) {
|
||||||
return <p>Something when wrong. Missing user payment data.</p>;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const { status, endDate, planId } = userPaymentData;
|
const { status, endDate, planId } = userPaymentData;
|
||||||
|
@ -181,6 +219,29 @@ const Page: NextPageWithLayout = () => {
|
||||||
>
|
>
|
||||||
<SubscriptionStatus />
|
<SubscriptionStatus />
|
||||||
</SettingsSection>
|
</SettingsSection>
|
||||||
|
<SettingsSection
|
||||||
|
title={<Trans i18nKey="support" defaults="Support" />}
|
||||||
|
description={
|
||||||
|
<Trans
|
||||||
|
i18nKey="supportDescription"
|
||||||
|
defaults="Need help with anything?"
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<div className="space-y-6">
|
||||||
|
<p>
|
||||||
|
<Trans
|
||||||
|
i18nKey="supportBilling"
|
||||||
|
defaults="Please reach out if you need any assistance."
|
||||||
|
/>
|
||||||
|
</p>
|
||||||
|
<Button asChild>
|
||||||
|
<Link href="mailto:support@rallly.co">
|
||||||
|
<Trans i18nKey="contactSupport" defaults="Contact Support" />
|
||||||
|
</Link>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</SettingsSection>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,8 +1,11 @@
|
||||||
|
import type { IncomingMessage, ServerResponse } from "http";
|
||||||
import { getIronSession } from "iron-session/edge";
|
import { getIronSession } from "iron-session/edge";
|
||||||
import { NextRequest, NextResponse } from "next/server";
|
|
||||||
|
|
||||||
import { sessionConfig } from "../session-config";
|
import { sessionConfig } from "../session-config";
|
||||||
|
|
||||||
export const getSession = async (req: NextRequest, res: NextResponse) => {
|
export const getSession = async (
|
||||||
|
req: Request | IncomingMessage,
|
||||||
|
res: Response | ServerResponse,
|
||||||
|
) => {
|
||||||
return getIronSession(req, res, sessionConfig);
|
return getIronSession(req, res, sessionConfig);
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
import type { IncomingMessage, ServerResponse } from "http";
|
||||||
|
import { getIronSession } from "iron-session";
|
||||||
import { withIronSessionApiRoute, withIronSessionSsr } from "iron-session/next";
|
import { withIronSessionApiRoute, withIronSessionSsr } from "iron-session/next";
|
||||||
import {
|
import {
|
||||||
GetServerSideProps,
|
GetServerSideProps,
|
||||||
|
@ -13,6 +15,13 @@ export function withSessionRoute(handler: NextApiHandler) {
|
||||||
return withIronSessionApiRoute(handler, sessionConfig);
|
return withIronSessionApiRoute(handler, sessionConfig);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const getSession = async (
|
||||||
|
req: Request | IncomingMessage,
|
||||||
|
res: Response | ServerResponse,
|
||||||
|
) => {
|
||||||
|
return getIronSession(req, res, sessionConfig);
|
||||||
|
};
|
||||||
|
|
||||||
export function withSessionSsr(
|
export function withSessionSsr(
|
||||||
handler: GetServerSideProps | GetServerSideProps[],
|
handler: GetServerSideProps | GetServerSideProps[],
|
||||||
options?: {
|
options?: {
|
||||||
|
|
|
@ -19,6 +19,7 @@
|
||||||
"@trpc/server": "^10.13.0",
|
"@trpc/server": "^10.13.0",
|
||||||
"iron-session": "^6.3.1",
|
"iron-session": "^6.3.1",
|
||||||
"spacetime": "^7.4.4",
|
"spacetime": "^7.4.4",
|
||||||
|
"stripe": "^13.2.0",
|
||||||
"superjson": "^1.12.2",
|
"superjson": "^1.12.2",
|
||||||
"timezone-soft": "^1.4.1"
|
"timezone-soft": "^1.4.1"
|
||||||
}
|
}
|
||||||
|
|
8
packages/backend/stripe.ts
Normal file
8
packages/backend/stripe.ts
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
import Stripe from "stripe";
|
||||||
|
|
||||||
|
export type { Stripe } from "stripe";
|
||||||
|
|
||||||
|
export const stripe = new Stripe(process.env.STRIPE_SECRET_KEY as string, {
|
||||||
|
apiVersion: "2023-08-16",
|
||||||
|
typescript: true,
|
||||||
|
});
|
|
@ -1,10 +1,10 @@
|
||||||
import { prisma } from "@rallly/database";
|
import { prisma } from "@rallly/database";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
import { publicProcedure, router } from "../trpc";
|
import { privateProcedure, router } from "../trpc";
|
||||||
|
|
||||||
export const user = router({
|
export const user = router({
|
||||||
getBilling: publicProcedure.query(async ({ ctx }) => {
|
getBilling: privateProcedure.query(async ({ ctx }) => {
|
||||||
return await prisma.userPaymentData.findUnique({
|
return await prisma.userPaymentData.findUnique({
|
||||||
select: {
|
select: {
|
||||||
subscriptionId: true,
|
subscriptionId: true,
|
||||||
|
@ -19,7 +19,50 @@ export const user = router({
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}),
|
}),
|
||||||
changeName: publicProcedure
|
subscription: privateProcedure.query(async ({ ctx }) => {
|
||||||
|
const user = await prisma.user.findUnique({
|
||||||
|
where: {
|
||||||
|
id: ctx.user.id,
|
||||||
|
},
|
||||||
|
select: {
|
||||||
|
subscription: {
|
||||||
|
select: {
|
||||||
|
active: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (user?.subscription?.active === true) {
|
||||||
|
return {
|
||||||
|
active: true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const userPaymentData = await prisma.userPaymentData.findUnique({
|
||||||
|
where: {
|
||||||
|
userId: ctx.user.id,
|
||||||
|
},
|
||||||
|
select: {
|
||||||
|
endDate: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (
|
||||||
|
userPaymentData?.endDate &&
|
||||||
|
userPaymentData.endDate.getTime() > Date.now()
|
||||||
|
) {
|
||||||
|
return {
|
||||||
|
active: true,
|
||||||
|
legacy: true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
active: false,
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
changeName: privateProcedure
|
||||||
.input(
|
.input(
|
||||||
z.object({
|
z.object({
|
||||||
name: z.string().min(1).max(100),
|
name: z.string().min(1).max(100),
|
||||||
|
|
|
@ -0,0 +1,27 @@
|
||||||
|
/*
|
||||||
|
Warnings:
|
||||||
|
|
||||||
|
- A unique constraint covering the columns `[subscription_id]` on the table `users` will be added. If there are existing duplicate values, this will fail.
|
||||||
|
|
||||||
|
*/
|
||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "users" ADD COLUMN "customer_id" TEXT,
|
||||||
|
ADD COLUMN "subscription_id" TEXT;
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "subscriptions" (
|
||||||
|
"id" TEXT NOT NULL,
|
||||||
|
"price_id" TEXT NOT NULL,
|
||||||
|
"active" BOOLEAN NOT NULL,
|
||||||
|
"currency" TEXT,
|
||||||
|
"interval" TEXT,
|
||||||
|
"interval_count" INTEGER,
|
||||||
|
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"period_start" TIMESTAMP(3) NOT NULL,
|
||||||
|
"period_end" TIMESTAMP(3) NOT NULL,
|
||||||
|
|
||||||
|
CONSTRAINT "subscriptions_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE UNIQUE INDEX "users_subscription_id_key" ON "users"("subscription_id");
|
|
@ -17,15 +17,18 @@ enum TimeFormat {
|
||||||
}
|
}
|
||||||
|
|
||||||
model User {
|
model User {
|
||||||
id String @id @default(cuid())
|
id String @id @default(cuid())
|
||||||
name String
|
name String
|
||||||
email String @unique() @db.Citext
|
email String @unique() @db.Citext
|
||||||
createdAt DateTime @default(now()) @map("created_at")
|
createdAt DateTime @default(now()) @map("created_at")
|
||||||
updatedAt DateTime? @updatedAt @map("updated_at")
|
updatedAt DateTime? @updatedAt @map("updated_at")
|
||||||
comments Comment[]
|
comments Comment[]
|
||||||
polls Poll[]
|
polls Poll[]
|
||||||
watcher Watcher[]
|
watcher Watcher[]
|
||||||
events Event[]
|
events Event[]
|
||||||
|
customerId String? @map("customer_id")
|
||||||
|
subscription Subscription? @relation(fields: [subscriptionId], references: [id])
|
||||||
|
subscriptionId String? @unique @map("subscription_id")
|
||||||
|
|
||||||
@@map("users")
|
@@map("users")
|
||||||
}
|
}
|
||||||
|
@ -52,6 +55,21 @@ model UserPaymentData {
|
||||||
@@map("user_payment_data")
|
@@map("user_payment_data")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
model Subscription {
|
||||||
|
id String @id
|
||||||
|
priceId String @map("price_id")
|
||||||
|
active Boolean
|
||||||
|
currency String?
|
||||||
|
interval String?
|
||||||
|
intervalCount Int? @map("interval_count")
|
||||||
|
createdAt DateTime @default(now()) @map("created_at")
|
||||||
|
periodStart DateTime @map("period_start")
|
||||||
|
periodEnd DateTime @map("period_end")
|
||||||
|
user User?
|
||||||
|
|
||||||
|
@@map("subscriptions")
|
||||||
|
}
|
||||||
|
|
||||||
model UserPreferences {
|
model UserPreferences {
|
||||||
userId String @id @map("user_id")
|
userId String @id @map("user_id")
|
||||||
timeZone String? @map("time_zone")
|
timeZone String? @map("time_zone")
|
||||||
|
|
|
@ -59,7 +59,6 @@
|
||||||
"EMAIL_PROVIDER",
|
"EMAIL_PROVIDER",
|
||||||
"MAINTENANCE_MODE",
|
"MAINTENANCE_MODE",
|
||||||
"NEXT_PUBLIC_APP_BASE_URL",
|
"NEXT_PUBLIC_APP_BASE_URL",
|
||||||
"NEXT_PUBLIC_SHORT_BASE_URL",
|
|
||||||
"NEXT_PUBLIC_APP_VERSION",
|
"NEXT_PUBLIC_APP_VERSION",
|
||||||
"NEXT_PUBLIC_BASE_URL",
|
"NEXT_PUBLIC_BASE_URL",
|
||||||
"NEXT_PUBLIC_BETA",
|
"NEXT_PUBLIC_BETA",
|
||||||
|
@ -76,6 +75,7 @@
|
||||||
"NEXT_PUBLIC_PRO_PLAN_ID_MONTHLY",
|
"NEXT_PUBLIC_PRO_PLAN_ID_MONTHLY",
|
||||||
"NEXT_PUBLIC_PRO_PLAN_ID_YEARLY",
|
"NEXT_PUBLIC_PRO_PLAN_ID_YEARLY",
|
||||||
"NEXT_PUBLIC_SENTRY_DSN",
|
"NEXT_PUBLIC_SENTRY_DSN",
|
||||||
|
"NEXT_PUBLIC_SHORT_BASE_URL",
|
||||||
"NEXT_PUBLIC_VERCEL_URL",
|
"NEXT_PUBLIC_VERCEL_URL",
|
||||||
"NODE_ENV",
|
"NODE_ENV",
|
||||||
"NOREPLY_EMAIL",
|
"NOREPLY_EMAIL",
|
||||||
|
@ -90,6 +90,10 @@
|
||||||
"SMTP_SECURE",
|
"SMTP_SECURE",
|
||||||
"SMTP_TLS_ENABLED",
|
"SMTP_TLS_ENABLED",
|
||||||
"SMTP_USER",
|
"SMTP_USER",
|
||||||
|
"STRIPE_MONTHLY_PRICE",
|
||||||
|
"STRIPE_SECRET_KEY",
|
||||||
|
"STRIPE_SIGNING_SECRET",
|
||||||
|
"STRIPE_YEARLY_PRICE",
|
||||||
"SUPPORT_EMAIL"
|
"SUPPORT_EMAIL"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
101
yarn.lock
101
yarn.lock
|
@ -3839,6 +3839,11 @@
|
||||||
resolved "https://registry.npmjs.org/@types/node/-/node-18.14.2.tgz"
|
resolved "https://registry.npmjs.org/@types/node/-/node-18.14.2.tgz"
|
||||||
integrity sha512-1uEQxww3DaghA0RxqHx0O0ppVlo43pJhepY51OxuQIKHpjbnYLA7vcdwioNPzIqmC2u3I/dmylcqjlh0e7AyUA==
|
integrity sha512-1uEQxww3DaghA0RxqHx0O0ppVlo43pJhepY51OxuQIKHpjbnYLA7vcdwioNPzIqmC2u3I/dmylcqjlh0e7AyUA==
|
||||||
|
|
||||||
|
"@types/node@>=8.1.0":
|
||||||
|
version "20.5.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/@types/node/-/node-20.5.0.tgz#7fc8636d5f1aaa3b21e6245e97d56b7f56702313"
|
||||||
|
integrity sha512-Mgq7eCtoTjT89FqNoTzzXg2XvCi5VMhRV6+I2aYanc6kQCBImeNaAYRs/DyoVqk1YEUJK5gN9VO7HRIdz4Wo3Q==
|
||||||
|
|
||||||
"@types/node@^12.7.1":
|
"@types/node@^12.7.1":
|
||||||
version "12.20.55"
|
version "12.20.55"
|
||||||
resolved "https://registry.npmjs.org/@types/node/-/node-12.20.55.tgz"
|
resolved "https://registry.npmjs.org/@types/node/-/node-12.20.55.tgz"
|
||||||
|
@ -4216,6 +4221,11 @@ append-buffer@^1.0.2:
|
||||||
dependencies:
|
dependencies:
|
||||||
buffer-equal "^1.0.0"
|
buffer-equal "^1.0.0"
|
||||||
|
|
||||||
|
arg@4.1.0:
|
||||||
|
version "4.1.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/arg/-/arg-4.1.0.tgz#583c518199419e0037abb74062c37f8519e575f0"
|
||||||
|
integrity sha512-ZWc51jO3qegGkVh8Hwpv636EkbesNV5ZNQPCtRa+0qytRYPEs9IYT9qITY9buezqUH5uqyzlWLcufrzU2rffdg==
|
||||||
|
|
||||||
arg@^4.1.0:
|
arg@^4.1.0:
|
||||||
version "4.1.3"
|
version "4.1.3"
|
||||||
resolved "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz"
|
resolved "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz"
|
||||||
|
@ -4548,6 +4558,11 @@ buffers@~0.1.1:
|
||||||
resolved "https://registry.npmjs.org/buffers/-/buffers-0.1.1.tgz"
|
resolved "https://registry.npmjs.org/buffers/-/buffers-0.1.1.tgz"
|
||||||
integrity sha512-9q/rDEGSb/Qsvv2qvzIzdluL5k7AaJOTrw23z9reQthrbF7is4CtlT0DXyO1oei2DCp4uojjzQ7igaSHp1kAEQ==
|
integrity sha512-9q/rDEGSb/Qsvv2qvzIzdluL5k7AaJOTrw23z9reQthrbF7is4CtlT0DXyO1oei2DCp4uojjzQ7igaSHp1kAEQ==
|
||||||
|
|
||||||
|
bytes@3.1.0:
|
||||||
|
version "3.1.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.1.0.tgz#f6cf7933a360e0588fa9fde85651cdc7f805d1f6"
|
||||||
|
integrity sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg==
|
||||||
|
|
||||||
call-bind@^1.0.0, call-bind@^1.0.2:
|
call-bind@^1.0.0, call-bind@^1.0.2:
|
||||||
version "1.0.2"
|
version "1.0.2"
|
||||||
resolved "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz"
|
resolved "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz"
|
||||||
|
@ -4857,6 +4872,11 @@ config-chain@^1.1.13:
|
||||||
ini "^1.3.4"
|
ini "^1.3.4"
|
||||||
proto-list "~1.2.1"
|
proto-list "~1.2.1"
|
||||||
|
|
||||||
|
content-type@1.0.4:
|
||||||
|
version "1.0.4"
|
||||||
|
resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.4.tgz#e138cc75e040c727b1966fe5e5f8c9aee256fe3b"
|
||||||
|
integrity sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==
|
||||||
|
|
||||||
convert-source-map@^1.5.0, convert-source-map@^1.7.0:
|
convert-source-map@^1.5.0, convert-source-map@^1.7.0:
|
||||||
version "1.9.0"
|
version "1.9.0"
|
||||||
resolved "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz"
|
resolved "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz"
|
||||||
|
@ -5122,6 +5142,11 @@ define-properties@^1.1.3, define-properties@^1.1.4:
|
||||||
has-property-descriptors "^1.0.0"
|
has-property-descriptors "^1.0.0"
|
||||||
object-keys "^1.1.1"
|
object-keys "^1.1.1"
|
||||||
|
|
||||||
|
depd@~1.1.2:
|
||||||
|
version "1.1.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/depd/-/depd-1.1.2.tgz#9bcd52e14c097763e749b274c4346ed2e560b5a9"
|
||||||
|
integrity sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ==
|
||||||
|
|
||||||
deprecation@^2.0.0, deprecation@^2.3.1:
|
deprecation@^2.0.0, deprecation@^2.3.1:
|
||||||
version "2.3.1"
|
version "2.3.1"
|
||||||
resolved "https://registry.npmjs.org/deprecation/-/deprecation-2.3.1.tgz"
|
resolved "https://registry.npmjs.org/deprecation/-/deprecation-2.3.1.tgz"
|
||||||
|
@ -6489,6 +6514,17 @@ htmlparser2@^8.0.1:
|
||||||
domutils "^3.0.1"
|
domutils "^3.0.1"
|
||||||
entities "^4.3.0"
|
entities "^4.3.0"
|
||||||
|
|
||||||
|
http-errors@1.7.3:
|
||||||
|
version "1.7.3"
|
||||||
|
resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.7.3.tgz#6c619e4f9c60308c38519498c14fbb10aacebb06"
|
||||||
|
integrity sha512-ZTTX0MWrsQ2ZAhA1cejAwDLycFsd7I7nVtnkT3Ol0aqodaKW+0CTZDQ1uBv5whptCnc8e8HeRRJxRs0kmm/Qfw==
|
||||||
|
dependencies:
|
||||||
|
depd "~1.1.2"
|
||||||
|
inherits "2.0.4"
|
||||||
|
setprototypeof "1.1.1"
|
||||||
|
statuses ">= 1.5.0 < 2"
|
||||||
|
toidentifier "1.0.0"
|
||||||
|
|
||||||
https-proxy-agent@^5.0.0:
|
https-proxy-agent@^5.0.0:
|
||||||
version "5.0.1"
|
version "5.0.1"
|
||||||
resolved "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz"
|
resolved "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz"
|
||||||
|
@ -6564,6 +6600,13 @@ i18next@^22.4.9:
|
||||||
dependencies:
|
dependencies:
|
||||||
"@babel/runtime" "^7.20.6"
|
"@babel/runtime" "^7.20.6"
|
||||||
|
|
||||||
|
iconv-lite@0.4.24:
|
||||||
|
version "0.4.24"
|
||||||
|
resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b"
|
||||||
|
integrity sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==
|
||||||
|
dependencies:
|
||||||
|
safer-buffer ">= 2.1.2 < 3"
|
||||||
|
|
||||||
iconv-lite@0.6.3:
|
iconv-lite@0.6.3:
|
||||||
version "0.6.3"
|
version "0.6.3"
|
||||||
resolved "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz"
|
resolved "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz"
|
||||||
|
@ -6620,7 +6663,7 @@ inflight@^1.0.4:
|
||||||
once "^1.3.0"
|
once "^1.3.0"
|
||||||
wrappy "1"
|
wrappy "1"
|
||||||
|
|
||||||
inherits@2, inherits@^2.0.1, inherits@^2.0.3, inherits@^2.0.4, inherits@~2.0.0, inherits@~2.0.3:
|
inherits@2, inherits@2.0.4, inherits@^2.0.1, inherits@^2.0.3, inherits@^2.0.4, inherits@~2.0.0, inherits@~2.0.3:
|
||||||
version "2.0.4"
|
version "2.0.4"
|
||||||
resolved "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz"
|
resolved "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz"
|
||||||
integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==
|
integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==
|
||||||
|
@ -7540,6 +7583,15 @@ merge2@^1.3.0, merge2@^1.4.1:
|
||||||
resolved "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz"
|
resolved "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz"
|
||||||
integrity sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==
|
integrity sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==
|
||||||
|
|
||||||
|
micro@^10.0.1:
|
||||||
|
version "10.0.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/micro/-/micro-10.0.1.tgz#2601e02b0dacd2eaee77e9de18f12b2e595c5951"
|
||||||
|
integrity sha512-9uwZSsUrqf6+4FLLpiPj5TRWQv5w5uJrJwsx1LR/TjqvQmKC1XnGQ9OHrFwR3cbZ46YqPqxO/XJCOpWnqMPw2Q==
|
||||||
|
dependencies:
|
||||||
|
arg "4.1.0"
|
||||||
|
content-type "1.0.4"
|
||||||
|
raw-body "2.4.1"
|
||||||
|
|
||||||
micromark-core-commonmark@^1.0.1:
|
micromark-core-commonmark@^1.0.1:
|
||||||
version "1.1.0"
|
version "1.1.0"
|
||||||
resolved "https://registry.yarnpkg.com/micromark-core-commonmark/-/micromark-core-commonmark-1.1.0.tgz#1386628df59946b2d39fb2edfd10f3e8e0a75bb8"
|
resolved "https://registry.yarnpkg.com/micromark-core-commonmark/-/micromark-core-commonmark-1.1.0.tgz#1386628df59946b2d39fb2edfd10f3e8e0a75bb8"
|
||||||
|
@ -8509,11 +8561,28 @@ pvutils@^1.1.3:
|
||||||
resolved "https://registry.npmjs.org/pvutils/-/pvutils-1.1.3.tgz"
|
resolved "https://registry.npmjs.org/pvutils/-/pvutils-1.1.3.tgz"
|
||||||
integrity sha512-pMpnA0qRdFp32b1sJl1wOJNxZLQ2cbQx+k6tjNtZ8CpvVhNqEPRgivZ2WOUev2YMajecdH7ctUPDvEe87nariQ==
|
integrity sha512-pMpnA0qRdFp32b1sJl1wOJNxZLQ2cbQx+k6tjNtZ8CpvVhNqEPRgivZ2WOUev2YMajecdH7ctUPDvEe87nariQ==
|
||||||
|
|
||||||
|
qs@^6.11.0:
|
||||||
|
version "6.11.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/qs/-/qs-6.11.2.tgz#64bea51f12c1f5da1bc01496f48ffcff7c69d7d9"
|
||||||
|
integrity sha512-tDNIz22aBzCDxLtVH++VnTfzxlfeK5CbqohpSqpJgj1Wg/cQbStNAz3NuqCs5vV+pjBsK4x4pN9HlVh7rcYRiA==
|
||||||
|
dependencies:
|
||||||
|
side-channel "^1.0.4"
|
||||||
|
|
||||||
queue-microtask@^1.2.2:
|
queue-microtask@^1.2.2:
|
||||||
version "1.2.3"
|
version "1.2.3"
|
||||||
resolved "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz"
|
resolved "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz"
|
||||||
integrity sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==
|
integrity sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==
|
||||||
|
|
||||||
|
raw-body@2.4.1:
|
||||||
|
version "2.4.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.4.1.tgz#30ac82f98bb5ae8c152e67149dac8d55153b168c"
|
||||||
|
integrity sha512-9WmIKF6mkvA0SLmA2Knm9+qj89e+j1zqgyn8aXGd7+nAduPoqgI9lO57SAZNn/Byzo5P7JhXTyg9PzaJbH73bA==
|
||||||
|
dependencies:
|
||||||
|
bytes "3.1.0"
|
||||||
|
http-errors "1.7.3"
|
||||||
|
iconv-lite "0.4.24"
|
||||||
|
unpipe "1.0.0"
|
||||||
|
|
||||||
react-big-calendar@^1.8.1:
|
react-big-calendar@^1.8.1:
|
||||||
version "1.8.1"
|
version "1.8.1"
|
||||||
resolved "https://registry.yarnpkg.com/react-big-calendar/-/react-big-calendar-1.8.1.tgz#07886a66086fcae16934572c5ace8c4c433dbbed"
|
resolved "https://registry.yarnpkg.com/react-big-calendar/-/react-big-calendar-1.8.1.tgz#07886a66086fcae16934572c5ace8c4c433dbbed"
|
||||||
|
@ -9041,7 +9110,7 @@ safe-regex-test@^1.0.0:
|
||||||
get-intrinsic "^1.1.3"
|
get-intrinsic "^1.1.3"
|
||||||
is-regex "^1.1.4"
|
is-regex "^1.1.4"
|
||||||
|
|
||||||
"safer-buffer@>= 2.1.2 < 3.0.0":
|
"safer-buffer@>= 2.1.2 < 3", "safer-buffer@>= 2.1.2 < 3.0.0":
|
||||||
version "2.1.2"
|
version "2.1.2"
|
||||||
resolved "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz"
|
resolved "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz"
|
||||||
integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==
|
integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==
|
||||||
|
@ -9116,6 +9185,11 @@ setimmediate@~1.0.4:
|
||||||
resolved "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz"
|
resolved "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz"
|
||||||
integrity sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==
|
integrity sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==
|
||||||
|
|
||||||
|
setprototypeof@1.1.1:
|
||||||
|
version "1.1.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.1.1.tgz#7e95acb24aa92f5885e0abef5ba131330d4ae683"
|
||||||
|
integrity sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw==
|
||||||
|
|
||||||
shallow-clone@^3.0.0:
|
shallow-clone@^3.0.0:
|
||||||
version "3.0.1"
|
version "3.0.1"
|
||||||
resolved "https://registry.npmjs.org/shallow-clone/-/shallow-clone-3.0.1.tgz"
|
resolved "https://registry.npmjs.org/shallow-clone/-/shallow-clone-3.0.1.tgz"
|
||||||
|
@ -9325,6 +9399,11 @@ stacktrace-parser@^0.1.10:
|
||||||
dependencies:
|
dependencies:
|
||||||
type-fest "^0.7.1"
|
type-fest "^0.7.1"
|
||||||
|
|
||||||
|
"statuses@>= 1.5.0 < 2":
|
||||||
|
version "1.5.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.5.0.tgz#161c7dac177659fd9811f43771fa99381478628c"
|
||||||
|
integrity sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA==
|
||||||
|
|
||||||
stop-iteration-iterator@^1.0.0:
|
stop-iteration-iterator@^1.0.0:
|
||||||
version "1.0.0"
|
version "1.0.0"
|
||||||
resolved "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.0.0.tgz"
|
resolved "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.0.0.tgz"
|
||||||
|
@ -9432,6 +9511,14 @@ strip-json-comments@^3.1.0, strip-json-comments@^3.1.1:
|
||||||
resolved "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz"
|
resolved "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz"
|
||||||
integrity sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==
|
integrity sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==
|
||||||
|
|
||||||
|
stripe@^13.2.0:
|
||||||
|
version "13.2.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/stripe/-/stripe-13.2.0.tgz#feb10555d55c871188b0e9bc9bdf0f8e52c42e5d"
|
||||||
|
integrity sha512-4a2UHpe/tyxP3sxSGhuKMgbW8hQnqSQIPMigXC8kW3P0+BpsITpKDP+xxriTMDkRAP0xTQwzxcqhfqB+/404Mg==
|
||||||
|
dependencies:
|
||||||
|
"@types/node" ">=8.1.0"
|
||||||
|
qs "^6.11.0"
|
||||||
|
|
||||||
striptags@^2.0.3:
|
striptags@^2.0.3:
|
||||||
version "2.2.1"
|
version "2.2.1"
|
||||||
resolved "https://registry.npmjs.org/striptags/-/striptags-2.2.1.tgz"
|
resolved "https://registry.npmjs.org/striptags/-/striptags-2.2.1.tgz"
|
||||||
|
@ -9685,6 +9772,11 @@ toggle-selection@^1.0.6:
|
||||||
resolved "https://registry.npmjs.org/toggle-selection/-/toggle-selection-1.0.6.tgz"
|
resolved "https://registry.npmjs.org/toggle-selection/-/toggle-selection-1.0.6.tgz"
|
||||||
integrity sha512-BiZS+C1OS8g/q2RRbJmy59xpyghNBqrr6k5L/uKBGRsTfxmu3ffiRnd8mlGPUVayg8pvfi5urfnu8TU7DVOkLQ==
|
integrity sha512-BiZS+C1OS8g/q2RRbJmy59xpyghNBqrr6k5L/uKBGRsTfxmu3ffiRnd8mlGPUVayg8pvfi5urfnu8TU7DVOkLQ==
|
||||||
|
|
||||||
|
toidentifier@1.0.0:
|
||||||
|
version "1.0.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/toidentifier/-/toidentifier-1.0.0.tgz#7e1be3470f1e77948bc43d94a3c8f4d7752ba553"
|
||||||
|
integrity sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw==
|
||||||
|
|
||||||
toposort@^2.0.2:
|
toposort@^2.0.2:
|
||||||
version "2.0.2"
|
version "2.0.2"
|
||||||
resolved "https://registry.npmjs.org/toposort/-/toposort-2.0.2.tgz"
|
resolved "https://registry.npmjs.org/toposort/-/toposort-2.0.2.tgz"
|
||||||
|
@ -10008,6 +10100,11 @@ universalify@^2.0.0:
|
||||||
resolved "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz"
|
resolved "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz"
|
||||||
integrity sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==
|
integrity sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==
|
||||||
|
|
||||||
|
unpipe@1.0.0:
|
||||||
|
version "1.0.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec"
|
||||||
|
integrity sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==
|
||||||
|
|
||||||
unzipper@^0.10.11:
|
unzipper@^0.10.11:
|
||||||
version "0.10.11"
|
version "0.10.11"
|
||||||
resolved "https://registry.npmjs.org/unzipper/-/unzipper-0.10.11.tgz"
|
resolved "https://registry.npmjs.org/unzipper/-/unzipper-0.10.11.tgz"
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue