mirror of
https://github.com/documenso/documenso.git
synced 2025-11-13 08:13:56 +10:00
feat: add free tier Stripe subscription
This commit is contained in:
@ -73,6 +73,7 @@ NEXT_PRIVATE_STRIPE_API_KEY=
|
|||||||
NEXT_PRIVATE_STRIPE_WEBHOOK_SECRET=
|
NEXT_PRIVATE_STRIPE_WEBHOOK_SECRET=
|
||||||
NEXT_PUBLIC_STRIPE_COMMUNITY_PLAN_MONTHLY_PRICE_ID=
|
NEXT_PUBLIC_STRIPE_COMMUNITY_PLAN_MONTHLY_PRICE_ID=
|
||||||
NEXT_PUBLIC_STRIPE_COMMUNITY_PLAN_YEARLY_PRICE_ID=
|
NEXT_PUBLIC_STRIPE_COMMUNITY_PLAN_YEARLY_PRICE_ID=
|
||||||
|
NEXT_PUBLIC_STRIPE_FREE_PLAN_ID=
|
||||||
|
|
||||||
# [[FEATURES]]
|
# [[FEATURES]]
|
||||||
# OPTIONAL: Leave blank to disable PostHog and feature flags.
|
# OPTIONAL: Leave blank to disable PostHog and feature flags.
|
||||||
|
|||||||
1
apps/marketing/process-env.d.ts
vendored
1
apps/marketing/process-env.d.ts
vendored
@ -7,6 +7,7 @@ declare namespace NodeJS {
|
|||||||
|
|
||||||
NEXT_PUBLIC_STRIPE_COMMUNITY_PLAN_MONTHLY_PRICE_ID: string;
|
NEXT_PUBLIC_STRIPE_COMMUNITY_PLAN_MONTHLY_PRICE_ID: string;
|
||||||
NEXT_PUBLIC_STRIPE_COMMUNITY_PLAN_YEARLY_PRICE_ID: string;
|
NEXT_PUBLIC_STRIPE_COMMUNITY_PLAN_YEARLY_PRICE_ID: string;
|
||||||
|
NEXT_PUBLIC_STRIPE_FREE_PLAN_ID?: string;
|
||||||
|
|
||||||
NEXT_PRIVATE_STRIPE_API_KEY: string;
|
NEXT_PRIVATE_STRIPE_API_KEY: string;
|
||||||
NEXT_PRIVATE_STRIPE_WEBHOOK_SECRET: string;
|
NEXT_PRIVATE_STRIPE_WEBHOOK_SECRET: string;
|
||||||
|
|||||||
1
apps/web/process-env.d.ts
vendored
1
apps/web/process-env.d.ts
vendored
@ -7,6 +7,7 @@ declare namespace NodeJS {
|
|||||||
|
|
||||||
NEXT_PUBLIC_STRIPE_COMMUNITY_PLAN_MONTHLY_PRICE_ID: string;
|
NEXT_PUBLIC_STRIPE_COMMUNITY_PLAN_MONTHLY_PRICE_ID: string;
|
||||||
NEXT_PUBLIC_STRIPE_COMMUNITY_PLAN_YEARLY_PRICE_ID: string;
|
NEXT_PUBLIC_STRIPE_COMMUNITY_PLAN_YEARLY_PRICE_ID: string;
|
||||||
|
NEXT_PUBLIC_STRIPE_FREE_PLAN_ID?: string;
|
||||||
|
|
||||||
NEXT_PRIVATE_STRIPE_API_KEY: string;
|
NEXT_PRIVATE_STRIPE_API_KEY: string;
|
||||||
NEXT_PRIVATE_STRIPE_WEBHOOK_SECRET: string;
|
NEXT_PRIVATE_STRIPE_WEBHOOK_SECRET: string;
|
||||||
|
|||||||
@ -0,0 +1,54 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { useState } from 'react';
|
||||||
|
|
||||||
|
import { Button } from '@documenso/ui/primitives/button';
|
||||||
|
import { useToast } from '@documenso/ui/primitives/use-toast';
|
||||||
|
|
||||||
|
import { createBillingPortal } from './create-billing-portal.action';
|
||||||
|
|
||||||
|
export default function BillingPortalButton() {
|
||||||
|
const { toast } = useToast();
|
||||||
|
|
||||||
|
const [isFetchingPortalUrl, setIsFetchingPortalUrl] = useState(false);
|
||||||
|
|
||||||
|
const handleFetchPortalUrl = async () => {
|
||||||
|
if (isFetchingPortalUrl) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsFetchingPortalUrl(true);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const sessionUrl = await createBillingPortal();
|
||||||
|
if (!sessionUrl) {
|
||||||
|
throw new Error('NO_SESSION');
|
||||||
|
}
|
||||||
|
|
||||||
|
window.open(sessionUrl, '_blank');
|
||||||
|
} catch (e) {
|
||||||
|
let description =
|
||||||
|
'We are unable to proceed to the billing portal at this time. Please try again, or contact support.';
|
||||||
|
|
||||||
|
if (e.message === 'CUSTOMER_NOT_FOUND') {
|
||||||
|
description =
|
||||||
|
'You do not currently have a customer record, this should not happen. Please contact support for assistance.';
|
||||||
|
}
|
||||||
|
|
||||||
|
toast({
|
||||||
|
title: 'Something went wrong',
|
||||||
|
description,
|
||||||
|
variant: 'destructive',
|
||||||
|
duration: 10000,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsFetchingPortalUrl(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Button onClick={async () => handleFetchPortalUrl()} loading={isFetchingPortalUrl}>
|
||||||
|
Manage Subscription
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -0,0 +1,80 @@
|
|||||||
|
'use server';
|
||||||
|
|
||||||
|
import {
|
||||||
|
getStripeCustomerByEmail,
|
||||||
|
getStripeCustomerById,
|
||||||
|
} from '@documenso/ee/server-only/stripe/get-customer';
|
||||||
|
import { getPortalSession } from '@documenso/ee/server-only/stripe/get-portal-session';
|
||||||
|
import { getRequiredServerComponentSession } from '@documenso/lib/next-auth/get-server-session';
|
||||||
|
import { Stripe, stripe } from '@documenso/lib/server-only/stripe';
|
||||||
|
import { getSubscriptionByUserId } from '@documenso/lib/server-only/subscription/get-subscription-by-user-id';
|
||||||
|
import { prisma } from '@documenso/prisma';
|
||||||
|
|
||||||
|
export const createBillingPortal = async () => {
|
||||||
|
const user = await getRequiredServerComponentSession();
|
||||||
|
|
||||||
|
const existingSubscription = await getSubscriptionByUserId({ userId: user.id });
|
||||||
|
|
||||||
|
let stripeCustomer: Stripe.Customer | null = null;
|
||||||
|
|
||||||
|
// Find the Stripe customer for the current user subscription.
|
||||||
|
if (existingSubscription) {
|
||||||
|
stripeCustomer = await getStripeCustomerById(existingSubscription.customerId);
|
||||||
|
|
||||||
|
if (!stripeCustomer) {
|
||||||
|
throw new Error('Missing Stripe customer for subscription');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback to check if a Stripe customer already exists for the current user.
|
||||||
|
stripeCustomer = await getStripeCustomerByEmail(user.email);
|
||||||
|
|
||||||
|
// Create a Stripe customer if it does not exist for the current user.
|
||||||
|
if (!stripeCustomer) {
|
||||||
|
stripeCustomer = await stripe.customers.create({
|
||||||
|
name: user.name ?? undefined,
|
||||||
|
email: user.email,
|
||||||
|
metadata: {
|
||||||
|
userId: user.id,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const stripeCustomerSubsriptions = stripeCustomer.subscriptions?.data ?? [];
|
||||||
|
|
||||||
|
// Create a free subscription for user if it does not exist.
|
||||||
|
if (!existingSubscription && stripeCustomerSubsriptions.length === 0) {
|
||||||
|
const newSubscription = await stripe.subscriptions.create({
|
||||||
|
customer: stripeCustomer.id,
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
plan: process.env.NEXT_PUBLIC_STRIPE_FREE_PLAN_ID,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
await prisma.subscription.upsert({
|
||||||
|
where: {
|
||||||
|
userId: user.id,
|
||||||
|
customerId: stripeCustomer.id,
|
||||||
|
},
|
||||||
|
create: {
|
||||||
|
userId: user.id,
|
||||||
|
customerId: stripeCustomer.id,
|
||||||
|
planId: process.env.NEXT_PUBLIC_STRIPE_FREE_PLAN_ID,
|
||||||
|
periodEnd: new Date(newSubscription.current_period_end * 1000),
|
||||||
|
status: 'ACTIVE',
|
||||||
|
},
|
||||||
|
update: {
|
||||||
|
planId: process.env.NEXT_PUBLIC_STRIPE_FREE_PLAN_ID,
|
||||||
|
periodEnd: new Date(newSubscription.current_period_end * 1000),
|
||||||
|
status: 'ACTIVE',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return getPortalSession({
|
||||||
|
customerId: stripeCustomer.id,
|
||||||
|
returnUrl: `${process.env.NEXT_PUBLIC_APP_URL}/settings/billing`,
|
||||||
|
});
|
||||||
|
};
|
||||||
@ -1,16 +1,15 @@
|
|||||||
import Link from 'next/link';
|
|
||||||
import { redirect } from 'next/navigation';
|
import { redirect } from 'next/navigation';
|
||||||
|
|
||||||
import { createCustomer } from '@documenso/ee/server-only/stripe/create-customer';
|
import { match } from 'ts-pattern';
|
||||||
import { getPortalSession } from '@documenso/ee/server-only/stripe/get-portal-session';
|
|
||||||
import { getRequiredServerComponentSession } from '@documenso/lib/next-auth/get-server-session';
|
import { getRequiredServerComponentSession } from '@documenso/lib/next-auth/get-server-session';
|
||||||
import { getServerComponentFlag } from '@documenso/lib/server-only/feature-flags/get-server-component-feature-flag';
|
import { Stripe, stripe } from '@documenso/lib/server-only/stripe';
|
||||||
import { getSubscriptionByUserId } from '@documenso/lib/server-only/subscription/get-subscription-by-user-id';
|
import { getSubscriptionByUserId } from '@documenso/lib/server-only/subscription/get-subscription-by-user-id';
|
||||||
import { SubscriptionStatus } from '@documenso/prisma/client';
|
|
||||||
import { Button } from '@documenso/ui/primitives/button';
|
|
||||||
|
|
||||||
import { LocaleDate } from '~/components/formatter/locale-date';
|
import { LocaleDate } from '~/components/formatter/locale-date';
|
||||||
|
|
||||||
|
import BillingPortalButton from './billing-portal-button';
|
||||||
|
|
||||||
export default async function BillingSettingsPage() {
|
export default async function BillingSettingsPage() {
|
||||||
const { user } = await getRequiredServerComponentSession();
|
const { user } = await getRequiredServerComponentSession();
|
||||||
|
|
||||||
@ -21,57 +20,74 @@ export default async function BillingSettingsPage() {
|
|||||||
redirect('/settings/profile');
|
redirect('/settings/profile');
|
||||||
}
|
}
|
||||||
|
|
||||||
const subscription = await getSubscriptionByUserId({ userId: user.id }).then(async (sub) => {
|
const subscription = await getSubscriptionByUserId({ userId: user.id });
|
||||||
if (sub) {
|
|
||||||
return sub;
|
let subscriptionProduct: Stripe.Product | null = null;
|
||||||
|
|
||||||
|
if (subscription?.planId) {
|
||||||
|
const foundSubscriptionProduct = (await stripe.products.list()).data.find(
|
||||||
|
(item) => item.default_price === subscription.planId,
|
||||||
|
);
|
||||||
|
|
||||||
|
subscriptionProduct = foundSubscriptionProduct ?? null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// If we don't have a customer record, create one as well as an empty subscription.
|
const isMissingOrInactiveOrFreePlan =
|
||||||
return createCustomer({ user });
|
!subscription ||
|
||||||
});
|
subscription.status === 'INACTIVE' ||
|
||||||
|
subscription?.planId === process.env.NEXT_PUBLIC_STRIPE_FREE_PLAN_ID;
|
||||||
let billingPortalUrl = '';
|
|
||||||
|
|
||||||
if (subscription.customerId) {
|
|
||||||
billingPortalUrl = await getPortalSession({
|
|
||||||
customerId: subscription.customerId,
|
|
||||||
returnUrl: `${process.env.NEXT_PUBLIC_WEBAPP_URL}/settings/billing`,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<h3 className="text-lg font-medium">Billing</h3>
|
<h3 className="text-lg font-medium">Billing</h3>
|
||||||
|
|
||||||
<p className="text-muted-foreground mt-2 text-sm">
|
<div className="mt-2 text-sm text-slate-500">
|
||||||
Your subscription is{' '}
|
{isMissingOrInactiveOrFreePlan && (
|
||||||
{subscription.status !== SubscriptionStatus.INACTIVE ? 'active' : 'inactive'}.
|
<p>
|
||||||
{subscription?.periodEnd && (
|
You are currently on the <span className="font-semibold">Free Plan</span>.
|
||||||
<>
|
</p>
|
||||||
{' '}
|
)}
|
||||||
Your next payment is due on{' '}
|
|
||||||
<span className="font-semibold">
|
{!isMissingOrInactiveOrFreePlan &&
|
||||||
<LocaleDate date={subscription.periodEnd} />
|
match(subscription.status)
|
||||||
|
.with('ACTIVE', () => (
|
||||||
|
<p>
|
||||||
|
{subscriptionProduct ? (
|
||||||
|
<span>
|
||||||
|
You are currently subscribed to{' '}
|
||||||
|
<span className="font-semibold">{subscriptionProduct.name}</span>
|
||||||
|
</span>
|
||||||
|
) : (
|
||||||
|
<span>You currently have an active plan</span>
|
||||||
|
)}
|
||||||
|
{subscription.periodEnd && (
|
||||||
|
<span>
|
||||||
|
{' '}
|
||||||
|
which is set to{' '}
|
||||||
|
{subscription.cancelAtPeriodEnd ? (
|
||||||
|
<span>
|
||||||
|
end on{' '}
|
||||||
|
<LocaleDate className="font-semibold" date={subscription.periodEnd} />.
|
||||||
|
</span>
|
||||||
|
) : (
|
||||||
|
<span>
|
||||||
|
automatically renew on{' '}
|
||||||
|
<LocaleDate className="font-semibold" date={subscription.periodEnd} />.
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
</span>
|
</span>
|
||||||
.
|
|
||||||
</>
|
|
||||||
)}
|
)}
|
||||||
</p>
|
</p>
|
||||||
|
))
|
||||||
|
.with('PAST_DUE', () => (
|
||||||
|
<p>Your current plan is past due. Please update your payment information.</p>
|
||||||
|
))
|
||||||
|
.otherwise(() => null)}
|
||||||
|
</div>
|
||||||
|
|
||||||
<hr className="my-4" />
|
<hr className="my-4" />
|
||||||
|
|
||||||
{billingPortalUrl && (
|
<BillingPortalButton />
|
||||||
<Button asChild>
|
|
||||||
<Link href={billingPortalUrl}>Manage Subscription</Link>
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{!billingPortalUrl && (
|
|
||||||
<p className="text-muted-foreground max-w-[60ch] text-base">
|
|
||||||
You do not currently have a customer record, this should not happen. Please contact
|
|
||||||
support for assistance.
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -3,6 +3,7 @@ import { NextApiRequest, NextApiResponse } from 'next';
|
|||||||
import { randomBytes } from 'crypto';
|
import { randomBytes } from 'crypto';
|
||||||
import { readFileSync } from 'fs';
|
import { readFileSync } from 'fs';
|
||||||
import { buffer } from 'micro';
|
import { buffer } from 'micro';
|
||||||
|
import { match } from 'ts-pattern';
|
||||||
|
|
||||||
import { insertImageInPDF } from '@documenso/lib/server-only/pdf/insert-image-in-pdf';
|
import { insertImageInPDF } from '@documenso/lib/server-only/pdf/insert-image-in-pdf';
|
||||||
import { insertTextInPDF } from '@documenso/lib/server-only/pdf/insert-text-in-pdf';
|
import { insertTextInPDF } from '@documenso/lib/server-only/pdf/insert-text-in-pdf';
|
||||||
@ -16,6 +17,7 @@ import {
|
|||||||
ReadStatus,
|
ReadStatus,
|
||||||
SendStatus,
|
SendStatus,
|
||||||
SigningStatus,
|
SigningStatus,
|
||||||
|
SubscriptionStatus,
|
||||||
} from '@documenso/prisma/client';
|
} from '@documenso/prisma/client';
|
||||||
|
|
||||||
const log = (...args: unknown[]) => console.log('[stripe]', ...args);
|
const log = (...args: unknown[]) => console.log('[stripe]', ...args);
|
||||||
@ -54,6 +56,18 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
|||||||
);
|
);
|
||||||
log('event-type:', event.type);
|
log('event-type:', event.type);
|
||||||
|
|
||||||
|
if (event.type === 'customer.subscription.updated') {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
||||||
|
const subscription = event.data.object as Stripe.Subscription;
|
||||||
|
|
||||||
|
await handleCustomerSubscriptionUpdated(subscription);
|
||||||
|
|
||||||
|
return res.status(200).json({
|
||||||
|
success: true,
|
||||||
|
message: 'Webhook received',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
if (event.type === 'checkout.session.completed') {
|
if (event.type === 'checkout.session.completed') {
|
||||||
// This is required since we don't want to create a guard for every event type
|
// This is required since we don't want to create a guard for every event type
|
||||||
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
||||||
@ -195,3 +209,29 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
|||||||
message: 'Unhandled webhook event',
|
message: 'Unhandled webhook event',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const handleCustomerSubscriptionUpdated = async (subscription: Stripe.Subscription) => {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
||||||
|
const plan = (subscription as unknown as Stripe.SubscriptionItem).plan;
|
||||||
|
|
||||||
|
const customerId =
|
||||||
|
typeof subscription.customer === 'string' ? subscription.customer : subscription.customer.id;
|
||||||
|
|
||||||
|
const status = match(subscription.status)
|
||||||
|
.with('active', () => SubscriptionStatus.ACTIVE)
|
||||||
|
.with('past_due', () => SubscriptionStatus.PAST_DUE)
|
||||||
|
.otherwise(() => SubscriptionStatus.INACTIVE);
|
||||||
|
|
||||||
|
await prisma.subscription.update({
|
||||||
|
where: {
|
||||||
|
customerId: customerId,
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
planId: plan.id,
|
||||||
|
status,
|
||||||
|
cancelAtPeriodEnd: subscription.cancel_at_period_end,
|
||||||
|
periodEnd: new Date(subscription.current_period_end * 1000),
|
||||||
|
updatedAt: new Date(),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|||||||
19
packages/ee/server-only/stripe/get-customer.ts
Normal file
19
packages/ee/server-only/stripe/get-customer.ts
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
import { stripe } from '@documenso/lib/server-only/stripe';
|
||||||
|
|
||||||
|
export const getStripeCustomerByEmail = async (email: string) => {
|
||||||
|
const foundStripeCustomers = await stripe.customers.list({
|
||||||
|
email,
|
||||||
|
});
|
||||||
|
|
||||||
|
return foundStripeCustomers.data[0] ?? null;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getStripeCustomerById = async (stripeCustomerId: string) => {
|
||||||
|
try {
|
||||||
|
const stripeCustomer = await stripe.customers.retrieve(stripeCustomerId);
|
||||||
|
|
||||||
|
return !stripeCustomer.deleted ? stripeCustomer : null;
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
};
|
||||||
@ -0,0 +1,17 @@
|
|||||||
|
/*
|
||||||
|
Warnings:
|
||||||
|
|
||||||
|
- A unique constraint covering the columns `[userId]` on the table `Subscription` will be added. If there are existing duplicate values, this will fail.
|
||||||
|
- Made the column `customerId` on table `Subscription` required. This step will fail if there are existing NULL values in that column.
|
||||||
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
DELETE FROM "Subscription"
|
||||||
|
WHERE "customerId" IS NULL;
|
||||||
|
|
||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "Subscription" ADD COLUMN "cancelAtPeriodEnd" BOOLEAN NOT NULL DEFAULT false,
|
||||||
|
ALTER COLUMN "customerId" SET NOT NULL;
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE UNIQUE INDEX "Subscription_userId_key" ON "Subscription"("userId");
|
||||||
@ -55,11 +55,12 @@ model Subscription {
|
|||||||
status SubscriptionStatus @default(INACTIVE)
|
status SubscriptionStatus @default(INACTIVE)
|
||||||
planId String?
|
planId String?
|
||||||
priceId String?
|
priceId String?
|
||||||
customerId String?
|
customerId String
|
||||||
periodEnd DateTime?
|
periodEnd DateTime?
|
||||||
userId Int
|
userId Int @unique
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
updatedAt DateTime @updatedAt
|
updatedAt DateTime @updatedAt
|
||||||
|
cancelAtPeriodEnd Boolean @default(false)
|
||||||
|
|
||||||
User User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
User User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||||
|
|
||||||
|
|||||||
1
packages/tsconfig/process-env.d.ts
vendored
1
packages/tsconfig/process-env.d.ts
vendored
@ -10,6 +10,7 @@ declare namespace NodeJS {
|
|||||||
|
|
||||||
NEXT_PUBLIC_STRIPE_COMMUNITY_PLAN_MONTHLY_PRICE_ID: string;
|
NEXT_PUBLIC_STRIPE_COMMUNITY_PLAN_MONTHLY_PRICE_ID: string;
|
||||||
NEXT_PUBLIC_STRIPE_COMMUNITY_PLAN_YEARLY_PRICE_ID: string;
|
NEXT_PUBLIC_STRIPE_COMMUNITY_PLAN_YEARLY_PRICE_ID: string;
|
||||||
|
NEXT_PUBLIC_STRIPE_FREE_PLAN_ID?: string;
|
||||||
|
|
||||||
NEXT_PRIVATE_STRIPE_API_KEY: string;
|
NEXT_PRIVATE_STRIPE_API_KEY: string;
|
||||||
NEXT_PRIVATE_STRIPE_WEBHOOK_SECRET: string;
|
NEXT_PRIVATE_STRIPE_WEBHOOK_SECRET: string;
|
||||||
|
|||||||
14
turbo.json
14
turbo.json
@ -2,8 +2,13 @@
|
|||||||
"$schema": "https://turbo.build/schema.json",
|
"$schema": "https://turbo.build/schema.json",
|
||||||
"pipeline": {
|
"pipeline": {
|
||||||
"build": {
|
"build": {
|
||||||
"dependsOn": ["^build"],
|
"dependsOn": [
|
||||||
"outputs": [".next/**", "!.next/cache/**"]
|
"^build"
|
||||||
|
],
|
||||||
|
"outputs": [
|
||||||
|
".next/**",
|
||||||
|
"!.next/cache/**"
|
||||||
|
]
|
||||||
},
|
},
|
||||||
"lint": {},
|
"lint": {},
|
||||||
"clean": {
|
"clean": {
|
||||||
@ -20,7 +25,9 @@
|
|||||||
"dependsOn": ["^build"]
|
"dependsOn": ["^build"]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"globalDependencies": ["**/.env.*local"],
|
"globalDependencies": [
|
||||||
|
"**/.env.*local"
|
||||||
|
],
|
||||||
"globalEnv": [
|
"globalEnv": [
|
||||||
"APP_VERSION",
|
"APP_VERSION",
|
||||||
"NEXTAUTH_URL",
|
"NEXTAUTH_URL",
|
||||||
@ -33,6 +40,7 @@
|
|||||||
"NEXT_PUBLIC_FEATURE_BILLING_ENABLED",
|
"NEXT_PUBLIC_FEATURE_BILLING_ENABLED",
|
||||||
"NEXT_PUBLIC_STRIPE_COMMUNITY_PLAN_YEARLY_PRICE_ID",
|
"NEXT_PUBLIC_STRIPE_COMMUNITY_PLAN_YEARLY_PRICE_ID",
|
||||||
"NEXT_PUBLIC_STRIPE_COMMUNITY_PLAN_MONTHLY_PRICE_ID",
|
"NEXT_PUBLIC_STRIPE_COMMUNITY_PLAN_MONTHLY_PRICE_ID",
|
||||||
|
"NEXT_PUBLIC_STRIPE_FREE_PLAN_ID",
|
||||||
"NEXT_PRIVATE_DATABASE_URL",
|
"NEXT_PRIVATE_DATABASE_URL",
|
||||||
"NEXT_PRIVATE_DIRECT_DATABASE_URL",
|
"NEXT_PRIVATE_DIRECT_DATABASE_URL",
|
||||||
"NEXT_PRIVATE_GOOGLE_CLIENT_ID",
|
"NEXT_PRIVATE_GOOGLE_CLIENT_ID",
|
||||||
|
|||||||
Reference in New Issue
Block a user