mirror of
https://github.com/sourcebot-dev/sourcebot.git
synced 2025-12-14 21:35:25 +00:00
Make billing optional (#232)
This commit is contained in:
parent
ad60c5f1e0
commit
e8667da1ca
13 changed files with 84 additions and 80 deletions
|
|
@ -49,6 +49,7 @@ REDIS_URL="redis://localhost:6379"
|
||||||
# STRIPE_SECRET_KEY: z.string().optional(),
|
# STRIPE_SECRET_KEY: z.string().optional(),
|
||||||
# STRIPE_PRODUCT_ID: z.string().optional(),
|
# STRIPE_PRODUCT_ID: z.string().optional(),
|
||||||
# STRIPE_WEBHOOK_SECRET: z.string().optional(),
|
# STRIPE_WEBHOOK_SECRET: z.string().optional(),
|
||||||
|
# STRIPE_ENABLE_TEST_CLOCKS=false
|
||||||
|
|
||||||
# Misc
|
# Misc
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -27,6 +27,7 @@ import { orgDomainSchema, orgNameSchema, repositoryQuerySchema } from "./lib/sch
|
||||||
import { RepositoryQuery } from "./lib/types";
|
import { RepositoryQuery } from "./lib/types";
|
||||||
import { MOBILE_UNSUPPORTED_SPLASH_SCREEN_DISMISSED_COOKIE_NAME } from "./lib/constants";
|
import { MOBILE_UNSUPPORTED_SPLASH_SCREEN_DISMISSED_COOKIE_NAME } from "./lib/constants";
|
||||||
import { stripeClient } from "./lib/stripe";
|
import { stripeClient } from "./lib/stripe";
|
||||||
|
import { IS_BILLING_ENABLED } from "./lib/stripe";
|
||||||
|
|
||||||
const ajv = new Ajv({
|
const ajv = new Ajv({
|
||||||
validateFormats: false,
|
validateFormats: false,
|
||||||
|
|
@ -174,19 +175,31 @@ export const completeOnboarding = async (domain: string): Promise<{ success: boo
|
||||||
return notFound();
|
return notFound();
|
||||||
}
|
}
|
||||||
|
|
||||||
const subscription = await fetchSubscription(domain);
|
// If billing is not enabled, we can just mark the org as onboarded.
|
||||||
if (isServiceError(subscription)) {
|
if (!IS_BILLING_ENABLED) {
|
||||||
return subscription;
|
await prisma.org.update({
|
||||||
}
|
where: { id: orgId },
|
||||||
|
data: {
|
||||||
|
isOnboarded: true,
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
await prisma.org.update({
|
// Else, validate that the org has an active subscription.
|
||||||
where: { id: orgId },
|
} else {
|
||||||
data: {
|
const subscriptionOrError = await fetchSubscription(domain);
|
||||||
isOnboarded: true,
|
if (isServiceError(subscriptionOrError)) {
|
||||||
stripeSubscriptionStatus: StripeSubscriptionStatus.ACTIVE,
|
return subscriptionOrError;
|
||||||
stripeLastUpdatedAt: new Date(),
|
|
||||||
}
|
}
|
||||||
});
|
|
||||||
|
await prisma.org.update({
|
||||||
|
where: { id: orgId },
|
||||||
|
data: {
|
||||||
|
isOnboarded: true,
|
||||||
|
stripeSubscriptionStatus: StripeSubscriptionStatus.ACTIVE,
|
||||||
|
stripeLastUpdatedAt: new Date(),
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
|
|
@ -708,9 +721,9 @@ export const redeemInvite = async (inviteId: string): Promise<{ success: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
const res = await prisma.$transaction(async (tx) => {
|
const res = await prisma.$transaction(async (tx) => {
|
||||||
// @note: we need to use the internal subscription fetch here since we would otherwise fail the `withOrgMembership` check.
|
if (IS_BILLING_ENABLED) {
|
||||||
const subscription = await _fetchSubscriptionForOrg(invite.orgId, tx);
|
// @note: we need to use the internal subscription fetch here since we would otherwise fail the `withOrgMembership` check.
|
||||||
if (subscription) {
|
const subscription = await _fetchSubscriptionForOrg(invite.orgId, tx);
|
||||||
if (isServiceError(subscription)) {
|
if (isServiceError(subscription)) {
|
||||||
return subscription;
|
return subscription;
|
||||||
}
|
}
|
||||||
|
|
@ -880,8 +893,7 @@ export const createOnboardingSubscription = async (domain: string) =>
|
||||||
} satisfies ServiceError;
|
} satisfies ServiceError;
|
||||||
}
|
}
|
||||||
|
|
||||||
// @nocheckin
|
const test_clock = env.STRIPE_ENABLE_TEST_CLOCKS === 'true' ? await stripeClient.testHelpers.testClocks.create({
|
||||||
const test_clock = env.AUTH_URL !== "https://app.sourcebot.dev" ? await stripeClient.testHelpers.testClocks.create({
|
|
||||||
frozen_time: Math.floor(Date.now() / 1000)
|
frozen_time: Math.floor(Date.now() / 1000)
|
||||||
}) : null;
|
}) : null;
|
||||||
|
|
||||||
|
|
@ -911,7 +923,7 @@ export const createOnboardingSubscription = async (domain: string) =>
|
||||||
})();
|
})();
|
||||||
|
|
||||||
const existingSubscription = await fetchSubscription(domain);
|
const existingSubscription = await fetchSubscription(domain);
|
||||||
if (existingSubscription && !isServiceError(existingSubscription)) {
|
if (!isServiceError(existingSubscription)) {
|
||||||
return {
|
return {
|
||||||
statusCode: StatusCodes.BAD_REQUEST,
|
statusCode: StatusCodes.BAD_REQUEST,
|
||||||
errorCode: ErrorCode.SUBSCRIPTION_ALREADY_EXISTS,
|
errorCode: ErrorCode.SUBSCRIPTION_ALREADY_EXISTS,
|
||||||
|
|
@ -1063,7 +1075,7 @@ export const getCustomerPortalSessionLink = async (domain: string): Promise<stri
|
||||||
}, /* minRequiredRole = */ OrgRole.OWNER)
|
}, /* minRequiredRole = */ OrgRole.OWNER)
|
||||||
);
|
);
|
||||||
|
|
||||||
export const fetchSubscription = (domain: string): Promise<Stripe.Subscription | null | ServiceError> =>
|
export const fetchSubscription = (domain: string): Promise<Stripe.Subscription | ServiceError> =>
|
||||||
withAuth(async (session) =>
|
withAuth(async (session) =>
|
||||||
withOrgMembership(session, domain, async ({ orgId }) => {
|
withOrgMembership(session, domain, async ({ orgId }) => {
|
||||||
return _fetchSubscriptionForOrg(orgId, prisma);
|
return _fetchSubscriptionForOrg(orgId, prisma);
|
||||||
|
|
@ -1167,8 +1179,8 @@ export const removeMemberFromOrg = async (memberId: string, domain: string): Pro
|
||||||
return notFound();
|
return notFound();
|
||||||
}
|
}
|
||||||
|
|
||||||
const subscription = await fetchSubscription(domain);
|
if (IS_BILLING_ENABLED) {
|
||||||
if (subscription) {
|
const subscription = await fetchSubscription(domain);
|
||||||
if (isServiceError(subscription)) {
|
if (isServiceError(subscription)) {
|
||||||
return subscription;
|
return subscription;
|
||||||
}
|
}
|
||||||
|
|
@ -1221,8 +1233,8 @@ export const leaveOrg = async (domain: string): Promise<{ success: boolean } | S
|
||||||
return notFound();
|
return notFound();
|
||||||
}
|
}
|
||||||
|
|
||||||
const subscription = await fetchSubscription(domain);
|
if (IS_BILLING_ENABLED) {
|
||||||
if (subscription) {
|
const subscription = await fetchSubscription(domain);
|
||||||
if (isServiceError(subscription)) {
|
if (isServiceError(subscription)) {
|
||||||
return subscription;
|
return subscription;
|
||||||
}
|
}
|
||||||
|
|
@ -1323,7 +1335,7 @@ export const dismissMobileUnsupportedSplashScreen = async () => {
|
||||||
|
|
||||||
////// Helpers ///////
|
////// Helpers ///////
|
||||||
|
|
||||||
const _fetchSubscriptionForOrg = async (orgId: number, prisma: Prisma.TransactionClient): Promise<Stripe.Subscription | null | ServiceError> => {
|
const _fetchSubscriptionForOrg = async (orgId: number, prisma: Prisma.TransactionClient): Promise<Stripe.Subscription | ServiceError> => {
|
||||||
const org = await prisma.org.findUnique({
|
const org = await prisma.org.findUnique({
|
||||||
where: {
|
where: {
|
||||||
id: orgId,
|
id: orgId,
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,7 @@ import { WarningNavIndicator } from "./warningNavIndicator";
|
||||||
import { ProgressNavIndicator } from "./progressNavIndicator";
|
import { ProgressNavIndicator } from "./progressNavIndicator";
|
||||||
import { SourcebotLogo } from "@/app/components/sourcebotLogo";
|
import { SourcebotLogo } from "@/app/components/sourcebotLogo";
|
||||||
import { TrialNavIndicator } from "./trialNavIndicator";
|
import { TrialNavIndicator } from "./trialNavIndicator";
|
||||||
|
import { IS_BILLING_ENABLED } from "@/lib/stripe";
|
||||||
const SOURCEBOT_DISCORD_URL = "https://discord.gg/6Fhp27x7Pb";
|
const SOURCEBOT_DISCORD_URL = "https://discord.gg/6Fhp27x7Pb";
|
||||||
const SOURCEBOT_GITHUB_URL = "https://github.com/sourcebot-dev/sourcebot";
|
const SOURCEBOT_GITHUB_URL = "https://github.com/sourcebot-dev/sourcebot";
|
||||||
|
|
||||||
|
|
@ -23,7 +23,7 @@ interface NavigationMenuProps {
|
||||||
export const NavigationMenu = async ({
|
export const NavigationMenu = async ({
|
||||||
domain,
|
domain,
|
||||||
}: NavigationMenuProps) => {
|
}: NavigationMenuProps) => {
|
||||||
const subscription = await getSubscriptionData(domain);
|
const subscription = IS_BILLING_ENABLED ? await getSubscriptionData(domain) : null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col w-screen h-fit">
|
<div className="flex flex-col w-screen h-fit">
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,7 @@ import { MobileUnsupportedSplashScreen } from "./components/mobileUnsupportedSpl
|
||||||
import { MOBILE_UNSUPPORTED_SPLASH_SCREEN_DISMISSED_COOKIE_NAME } from "@/lib/constants";
|
import { MOBILE_UNSUPPORTED_SPLASH_SCREEN_DISMISSED_COOKIE_NAME } from "@/lib/constants";
|
||||||
import { SyntaxReferenceGuide } from "./components/syntaxReferenceGuide";
|
import { SyntaxReferenceGuide } from "./components/syntaxReferenceGuide";
|
||||||
import { SyntaxGuideProvider } from "./components/syntaxGuideProvider";
|
import { SyntaxGuideProvider } from "./components/syntaxGuideProvider";
|
||||||
|
import { IS_BILLING_ENABLED } from "@/lib/stripe";
|
||||||
|
|
||||||
interface LayoutProps {
|
interface LayoutProps {
|
||||||
children: React.ReactNode,
|
children: React.ReactNode,
|
||||||
|
|
@ -56,19 +57,21 @@ export default async function Layout({
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const subscription = await fetchSubscription(domain);
|
if (IS_BILLING_ENABLED) {
|
||||||
if (
|
const subscription = await fetchSubscription(domain);
|
||||||
subscription &&
|
if (
|
||||||
(
|
subscription &&
|
||||||
isServiceError(subscription) ||
|
(
|
||||||
(subscription.status !== "active" && subscription.status !== "trialing")
|
isServiceError(subscription) ||
|
||||||
)
|
(subscription.status !== "active" && subscription.status !== "trialing")
|
||||||
) {
|
)
|
||||||
return (
|
) {
|
||||||
<UpgradeGuard>
|
return (
|
||||||
{children}
|
<UpgradeGuard>
|
||||||
</UpgradeGuard>
|
{children}
|
||||||
)
|
</UpgradeGuard>
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const headersList = await headers();
|
const headersList = await headers();
|
||||||
|
|
|
||||||
|
|
@ -1,30 +0,0 @@
|
||||||
'use client';
|
|
||||||
|
|
||||||
import Link from "next/link";
|
|
||||||
import { OnboardingSteps } from "@/lib/constants";
|
|
||||||
import useCaptureEvent from "@/hooks/useCaptureEvent";
|
|
||||||
|
|
||||||
interface SkipOnboardingButtonProps {
|
|
||||||
currentStep: OnboardingSteps;
|
|
||||||
lastRequiredStep: OnboardingSteps;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const SkipOnboardingButton = ({ currentStep, lastRequiredStep }: SkipOnboardingButtonProps) => {
|
|
||||||
const captureEvent = useCaptureEvent();
|
|
||||||
|
|
||||||
const handleClick = () => {
|
|
||||||
captureEvent('wa_onboard_skip_onboarding', {
|
|
||||||
step: currentStep
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Link
|
|
||||||
className="text-sm text-muted-foreground underline cursor-pointer mt-12"
|
|
||||||
href={`?step=${lastRequiredStep}`}
|
|
||||||
onClick={handleClick}
|
|
||||||
>
|
|
||||||
Skip onboarding
|
|
||||||
</Link>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
@ -7,7 +7,7 @@ import { InviteTeam } from "./components/inviteTeam";
|
||||||
import { CompleteOnboarding } from "./components/completeOnboarding";
|
import { CompleteOnboarding } from "./components/completeOnboarding";
|
||||||
import { Checkout } from "./components/checkout";
|
import { Checkout } from "./components/checkout";
|
||||||
import { LogoutEscapeHatch } from "@/app/components/logoutEscapeHatch";
|
import { LogoutEscapeHatch } from "@/app/components/logoutEscapeHatch";
|
||||||
import SecurityCard from "@/app/components/securityCard";
|
import { IS_BILLING_ENABLED } from "@/lib/stripe";
|
||||||
|
|
||||||
interface OnboardProps {
|
interface OnboardProps {
|
||||||
params: {
|
params: {
|
||||||
|
|
@ -34,13 +34,14 @@ export default async function Onboard({ params, searchParams }: OnboardProps) {
|
||||||
if (
|
if (
|
||||||
!Object.values(OnboardingSteps)
|
!Object.values(OnboardingSteps)
|
||||||
.filter(s => s !== OnboardingSteps.CreateOrg)
|
.filter(s => s !== OnboardingSteps.CreateOrg)
|
||||||
|
.filter(s => !IS_BILLING_ENABLED ? s !== OnboardingSteps.Checkout : true)
|
||||||
.map(s => s.toString())
|
.map(s => s.toString())
|
||||||
.includes(step)
|
.includes(step)
|
||||||
) {
|
) {
|
||||||
redirect(`/${params.domain}/onboard?step=${OnboardingSteps.ConnectCodeHost}`);
|
redirect(`/${params.domain}/onboard?step=${OnboardingSteps.ConnectCodeHost}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const lastRequiredStep = OnboardingSteps.Checkout;
|
const lastRequiredStep = IS_BILLING_ENABLED ? OnboardingSteps.Checkout : OnboardingSteps.Complete;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col items-center py-12 px-4 sm:px-12 min-h-screen bg-backgroundSecondary relative">
|
<div className="flex flex-col items-center py-12 px-4 sm:px-12 min-h-screen bg-backgroundSecondary relative">
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,8 @@ import { ManageSubscriptionButton } from "./manageSubscriptionButton"
|
||||||
import { getSubscriptionData, getCurrentUserRole, getSubscriptionBillingEmail } from "@/actions"
|
import { getSubscriptionData, getCurrentUserRole, getSubscriptionBillingEmail } from "@/actions"
|
||||||
import { isServiceError } from "@/lib/utils"
|
import { isServiceError } from "@/lib/utils"
|
||||||
import { ChangeBillingEmailCard } from "./changeBillingEmailCard"
|
import { ChangeBillingEmailCard } from "./changeBillingEmailCard"
|
||||||
|
import { notFound } from "next/navigation"
|
||||||
|
import { IS_BILLING_ENABLED } from "@/lib/stripe"
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
title: "Billing | Settings",
|
title: "Billing | Settings",
|
||||||
|
|
@ -20,6 +22,10 @@ interface BillingPageProps {
|
||||||
export default async function BillingPage({
|
export default async function BillingPage({
|
||||||
params: { domain },
|
params: { domain },
|
||||||
}: BillingPageProps) {
|
}: BillingPageProps) {
|
||||||
|
if (!IS_BILLING_ENABLED) {
|
||||||
|
notFound();
|
||||||
|
}
|
||||||
|
|
||||||
const subscription = await getSubscriptionData(domain)
|
const subscription = await getSubscriptionData(domain)
|
||||||
|
|
||||||
if (isServiceError(subscription)) {
|
if (isServiceError(subscription)) {
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ import { Metadata } from "next"
|
||||||
import { SidebarNav } from "./components/sidebar-nav"
|
import { SidebarNav } from "./components/sidebar-nav"
|
||||||
import { NavigationMenu } from "../components/navigationMenu"
|
import { NavigationMenu } from "../components/navigationMenu"
|
||||||
import { Header } from "./components/header";
|
import { Header } from "./components/header";
|
||||||
|
import { IS_BILLING_ENABLED } from "@/lib/stripe";
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
title: "Settings",
|
title: "Settings",
|
||||||
}
|
}
|
||||||
|
|
@ -19,10 +20,12 @@ export default function SettingsLayout({
|
||||||
title: "General",
|
title: "General",
|
||||||
href: `/${domain}/settings`,
|
href: `/${domain}/settings`,
|
||||||
},
|
},
|
||||||
{
|
...(IS_BILLING_ENABLED ? [
|
||||||
title: "Billing",
|
{
|
||||||
href: `/${domain}/settings/billing`,
|
title: "Billing",
|
||||||
},
|
href: `/${domain}/settings/billing`,
|
||||||
|
}
|
||||||
|
] : []),
|
||||||
{
|
{
|
||||||
title: "Members",
|
title: "Members",
|
||||||
href: `/${domain}/settings/members`,
|
href: `/${domain}/settings/members`,
|
||||||
|
|
|
||||||
|
|
@ -29,9 +29,10 @@ export const inviteMemberFormSchema = z.object({
|
||||||
|
|
||||||
interface InviteMemberCardProps {
|
interface InviteMemberCardProps {
|
||||||
currentUserRole: OrgRole;
|
currentUserRole: OrgRole;
|
||||||
|
isBillingEnabled: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const InviteMemberCard = ({ currentUserRole }: InviteMemberCardProps) => {
|
export const InviteMemberCard = ({ currentUserRole, isBillingEnabled }: InviteMemberCardProps) => {
|
||||||
const [isInviteDialogOpen, setIsInviteDialogOpen] = useState(false);
|
const [isInviteDialogOpen, setIsInviteDialogOpen] = useState(false);
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
const domain = useDomain();
|
const domain = useDomain();
|
||||||
|
|
@ -144,7 +145,7 @@ export const InviteMemberCard = ({ currentUserRole }: InviteMemberCardProps) =>
|
||||||
<AlertDialogHeader>
|
<AlertDialogHeader>
|
||||||
<AlertDialogTitle>Invite Team Members</AlertDialogTitle>
|
<AlertDialogTitle>Invite Team Members</AlertDialogTitle>
|
||||||
<AlertDialogDescription>
|
<AlertDialogDescription>
|
||||||
{`Your team is growing! By confirming, you will be inviting ${form.getValues().emails.length} new members to your organization. Your subscription's seat count will be adjusted when a member accepts their invitation.`}
|
{`Your team is growing! By confirming, you will be inviting ${form.getValues().emails.length} new members to your organization. ${isBillingEnabled ? "Your subscription's seat count will be adjusted when a member accepts their invitation." : ""}`}
|
||||||
</AlertDialogDescription>
|
</AlertDialogDescription>
|
||||||
</AlertDialogHeader>
|
</AlertDialogHeader>
|
||||||
<div className="border rounded-lg overflow-hidden">
|
<div className="border rounded-lg overflow-hidden">
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@ import { Tabs, TabsContent } from "@/components/ui/tabs";
|
||||||
import { TabSwitcher } from "@/components/ui/tab-switcher";
|
import { TabSwitcher } from "@/components/ui/tab-switcher";
|
||||||
import { InvitesList } from "./components/invitesList";
|
import { InvitesList } from "./components/invitesList";
|
||||||
import { getOrgInvites } from "@/actions";
|
import { getOrgInvites } from "@/actions";
|
||||||
|
import { IS_BILLING_ENABLED } from "@/lib/stripe";
|
||||||
interface MembersSettingsPageProps {
|
interface MembersSettingsPageProps {
|
||||||
params: {
|
params: {
|
||||||
domain: string
|
domain: string
|
||||||
|
|
@ -61,6 +61,7 @@ export default async function MembersSettingsPage({ params: { domain }, searchPa
|
||||||
|
|
||||||
<InviteMemberCard
|
<InviteMemberCard
|
||||||
currentUserRole={userRoleInOrg}
|
currentUserRole={userRoleInOrg}
|
||||||
|
isBillingEnabled={IS_BILLING_ENABLED}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Tabs value={currentTab}>
|
<Tabs value={currentTab}>
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
import { SourcebotLogo } from "@/app/components/sourcebotLogo"
|
import { SourcebotLogo } from "@/app/components/sourcebotLogo"
|
||||||
import { OnboardingSteps } from "@/lib/constants";
|
import { OnboardingSteps } from "@/lib/constants";
|
||||||
|
import { IS_BILLING_ENABLED } from "@/lib/stripe";
|
||||||
|
|
||||||
interface OnboardHeaderProps {
|
interface OnboardHeaderProps {
|
||||||
title: string
|
title: string
|
||||||
|
|
@ -8,7 +9,9 @@ interface OnboardHeaderProps {
|
||||||
}
|
}
|
||||||
|
|
||||||
export const OnboardHeader = ({ title, description, step: currentStep }: OnboardHeaderProps) => {
|
export const OnboardHeader = ({ title, description, step: currentStep }: OnboardHeaderProps) => {
|
||||||
const steps = Object.values(OnboardingSteps).filter(s => s !== OnboardingSteps.Complete);
|
const steps = Object.values(OnboardingSteps)
|
||||||
|
.filter(s => s !== OnboardingSteps.Complete)
|
||||||
|
.filter(s => !IS_BILLING_ENABLED ? s !== OnboardingSteps.Checkout : true);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col items-center text-center mb-10">
|
<div className="flex flex-col items-center text-center mb-10">
|
||||||
|
|
|
||||||
|
|
@ -28,6 +28,7 @@ export const env = createEnv({
|
||||||
STRIPE_SECRET_KEY: z.string().optional(),
|
STRIPE_SECRET_KEY: z.string().optional(),
|
||||||
STRIPE_PRODUCT_ID: z.string().optional(),
|
STRIPE_PRODUCT_ID: z.string().optional(),
|
||||||
STRIPE_WEBHOOK_SECRET: z.string().optional(),
|
STRIPE_WEBHOOK_SECRET: z.string().optional(),
|
||||||
|
STRIPE_ENABLE_TEST_CLOCKS: booleanSchema.default('false'),
|
||||||
|
|
||||||
// Misc
|
// Misc
|
||||||
CONFIG_MAX_REPOS_NO_TOKEN: z.number().default(500),
|
CONFIG_MAX_REPOS_NO_TOKEN: z.number().default(500),
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,9 @@ import 'server-only';
|
||||||
import { env } from '@/env.mjs'
|
import { env } from '@/env.mjs'
|
||||||
import Stripe from "stripe";
|
import Stripe from "stripe";
|
||||||
|
|
||||||
|
export const IS_BILLING_ENABLED = env.STRIPE_SECRET_KEY !== undefined;
|
||||||
|
|
||||||
export const stripeClient =
|
export const stripeClient =
|
||||||
env.STRIPE_SECRET_KEY
|
IS_BILLING_ENABLED
|
||||||
? new Stripe(env.STRIPE_SECRET_KEY)
|
? new Stripe(env.STRIPE_SECRET_KEY!)
|
||||||
: undefined;
|
: undefined;
|
||||||
Loading…
Reference in a new issue