make non-core env variables optional
This commit is contained in:
parent
e05b05f673
commit
29f2da7379
13 changed files with 128 additions and 102 deletions
16
.env.example
16
.env.example
|
|
@ -1,15 +1,9 @@
|
|||
# Prisma
|
||||
# https://www.prisma.io/docs/reference/database-reference/connection-urls#env
|
||||
# Database
|
||||
DATABASE_URL=""
|
||||
|
||||
# Next Auth
|
||||
# You can generate a new secret on the command line with:
|
||||
# openssl rand -base64 32
|
||||
# https://next-auth.js.org/configuration/options#secret
|
||||
# Authjs
|
||||
NEXTAUTH_URL="http://localhost:3000/"
|
||||
NEXTAUTH_SECRET=""
|
||||
NEXTAUTH_URL="http://localhost:3000"
|
||||
|
||||
# Next Auth Provider
|
||||
GOOGLE_CLIENT_ID=""
|
||||
GOOGLE_CLIENT_SECRET=""
|
||||
GITHUB_ID=""
|
||||
|
|
@ -38,10 +32,10 @@ NEXT_PUBLIC_CRISP_WEBSITE_ID=""
|
|||
|
||||
# posthog
|
||||
NEXT_PUBLIC_POSTHOG_KEY=""
|
||||
NEXT_PUBLIC_POSTHOG_HOST=""
|
||||
NEXT_PUBLIC_POSTHOG_HOST="https://app.posthog.com"
|
||||
NEXT_PUBLIC_POSTHOG_PROXY_HOST="/ioafe"
|
||||
POSTHOG_PROXY_PATH="ioafe"
|
||||
|
||||
# redis for ratelimiting
|
||||
UPSTASH_REDIS_REST_URL=""
|
||||
UPSTASH_REDIS_REST_TOKEN="="
|
||||
UPSTASH_REDIS_REST_TOKEN=""
|
||||
|
|
@ -4,7 +4,9 @@ import { env } from "~/env.mjs";
|
|||
|
||||
export default function CrispChat() {
|
||||
useEffect(() => {
|
||||
Crisp.configure(env.NEXT_PUBLIC_CRISP_WEBSITE_ID);
|
||||
if (env.NEXT_PUBLIC_CRISP_WEBSITE_ID) {
|
||||
Crisp.configure(env.NEXT_PUBLIC_CRISP_WEBSITE_ID);
|
||||
}
|
||||
}, []);
|
||||
|
||||
return null;
|
||||
|
|
|
|||
18
src/env.mjs
18
src/env.mjs
|
|
@ -28,13 +28,13 @@ const server = z.object({
|
|||
AWS_KEY_ID: z.string(),
|
||||
AWS_SECRET_ACCESS_KEY: z.string(),
|
||||
AWS_BUCKET_NAME: z.string(),
|
||||
STRIPE_SECRET_KEY: z.string(),
|
||||
STRIPE_WEBHOOK_SECRET: z.string(),
|
||||
STRIPE_MONTHLY_PRICE_ID: z.string(),
|
||||
STRIPE_ANNUAL_PRICE_ID: z.string(),
|
||||
STRIPE_SECRET_KEY: z.string().nullish(),
|
||||
STRIPE_WEBHOOK_SECRET: z.string().nullish(),
|
||||
STRIPE_MONTHLY_PRICE_ID: z.string().nullish(),
|
||||
STRIPE_ANNUAL_PRICE_ID: z.string().nullish(),
|
||||
POSTHOG_PROXY_PATH: z.string(),
|
||||
UPSTASH_REDIS_REST_URL: z.string(),
|
||||
UPSTASH_REDIS_REST_TOKEN: z.string(),
|
||||
UPSTASH_REDIS_REST_URL: z.string().nullish(),
|
||||
UPSTASH_REDIS_REST_TOKEN: z.string().nullish(),
|
||||
});
|
||||
|
||||
/**
|
||||
|
|
@ -43,9 +43,9 @@ const server = z.object({
|
|||
*/
|
||||
const client = z.object({
|
||||
// NEXT_PUBLIC_CLIENTVAR: z.string().min(1),
|
||||
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY: z.string(),
|
||||
NEXT_PUBLIC_CRISP_WEBSITE_ID: z.string(),
|
||||
NEXT_PUBLIC_POSTHOG_KEY: z.string(),
|
||||
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY: z.string().nullish(),
|
||||
NEXT_PUBLIC_CRISP_WEBSITE_ID: z.string().nullish(),
|
||||
NEXT_PUBLIC_POSTHOG_KEY: z.string().nullish(),
|
||||
NEXT_PUBLIC_POSTHOG_HOST: z.string(),
|
||||
NEXT_PUBLIC_POSTHOG_PROXY_HOST: z.string(),
|
||||
});
|
||||
|
|
|
|||
|
|
@ -12,12 +12,12 @@ import { env } from "~/env.mjs";
|
|||
import { type ReactNode, useEffect } from "react";
|
||||
|
||||
// Check that PostHog is client-side (used to handle Next.js SSR)
|
||||
if (typeof window !== "undefined") {
|
||||
if (typeof window !== "undefined" && !!env.NEXT_PUBLIC_POSTHOG_KEY) {
|
||||
posthog.init(env.NEXT_PUBLIC_POSTHOG_KEY, {
|
||||
api_host: env.NEXT_PUBLIC_POSTHOG_PROXY_HOST,
|
||||
// Enable debug mode in development
|
||||
loaded: (posthog) => {
|
||||
if (process.env.NODE_ENV === "development") posthog.debug(false);
|
||||
if (process.env.NODE_ENV === "development") posthog.debug();
|
||||
},
|
||||
});
|
||||
}
|
||||
|
|
@ -47,16 +47,16 @@ const PostHogIdentificationWrapper = ({
|
|||
const posthog = usePostHog();
|
||||
|
||||
useEffect(() => {
|
||||
if (!posthog) return;
|
||||
if (!posthog?.__loaded) return;
|
||||
if (status === "authenticated") {
|
||||
const { id, name, email, stripeSubscriptionStatus } = session?.user;
|
||||
posthog.identify(id, {
|
||||
posthog?.identify(id, {
|
||||
name,
|
||||
email,
|
||||
stripeSubscriptionStatus,
|
||||
});
|
||||
} else if (status === "unauthenticated") {
|
||||
posthog.reset();
|
||||
posthog?.reset();
|
||||
}
|
||||
}, [posthog, session, status]);
|
||||
|
||||
|
|
|
|||
|
|
@ -24,6 +24,10 @@ export default async function handler(
|
|||
req: NextApiRequest,
|
||||
res: NextApiResponse
|
||||
) {
|
||||
if (!webhookSecret || !stripe) {
|
||||
return res.status(500).end("Stripe env variables not set");
|
||||
}
|
||||
|
||||
if (req.method === "POST") {
|
||||
const buf = await buffer(req);
|
||||
const sig = req.headers["stripe-signature"];
|
||||
|
|
@ -72,11 +76,11 @@ export default async function handler(
|
|||
const userId = subscription.metadata.userId;
|
||||
|
||||
if (userId) {
|
||||
posthog.capture({
|
||||
posthog?.capture({
|
||||
distinctId: userId,
|
||||
event: "stripe invoice.payment_failed",
|
||||
});
|
||||
void posthog.shutdownAsync();
|
||||
void posthog?.shutdownAsync();
|
||||
}
|
||||
break;
|
||||
case "customer.subscription.deleted":
|
||||
|
|
|
|||
|
|
@ -8,6 +8,14 @@ export const stripeRouter = createTRPCRouter({
|
|||
.input(z.object({ billedAnnually: z.boolean() }))
|
||||
.mutation(
|
||||
async ({ ctx: { prisma, stripe, session, req, posthog }, input }) => {
|
||||
if (
|
||||
!stripe ||
|
||||
!env.STRIPE_ANNUAL_PRICE_ID ||
|
||||
!env.STRIPE_MONTHLY_PRICE_ID
|
||||
) {
|
||||
throw new Error("Stripe env variables not set");
|
||||
}
|
||||
|
||||
const customerId = await getOrCreateStripeCustomerIdForUser({
|
||||
prisma,
|
||||
stripe,
|
||||
|
|
@ -50,20 +58,24 @@ export const stripeRouter = createTRPCRouter({
|
|||
throw new Error("Could not create checkout session");
|
||||
}
|
||||
|
||||
posthog.capture({
|
||||
posthog?.capture({
|
||||
distinctId: session.user.id,
|
||||
event: "visiting checkout page",
|
||||
properties: {
|
||||
billingCycle: input.billedAnnually ? "annual" : "monthly",
|
||||
},
|
||||
});
|
||||
void posthog.shutdownAsync();
|
||||
void posthog?.shutdownAsync();
|
||||
|
||||
return { checkoutUrl: checkoutSession.url };
|
||||
}
|
||||
),
|
||||
createBillingPortalSession: protectedProcedure.mutation(
|
||||
async ({ ctx: { stripe, session, prisma, req, posthog } }) => {
|
||||
if (!stripe) {
|
||||
throw new Error("Stripe env variables not set");
|
||||
}
|
||||
|
||||
const customerId = await getOrCreateStripeCustomerIdForUser({
|
||||
prisma,
|
||||
stripe,
|
||||
|
|
@ -89,14 +101,14 @@ export const stripeRouter = createTRPCRouter({
|
|||
throw new Error("Could not create billing portal session");
|
||||
}
|
||||
|
||||
posthog.capture({
|
||||
posthog?.capture({
|
||||
distinctId: session.user.id,
|
||||
event: "visiting billing portal page",
|
||||
properties: {
|
||||
stripeSubscriptionStatus: session.user.stripeSubscriptionStatus,
|
||||
},
|
||||
});
|
||||
void posthog.shutdownAsync();
|
||||
void posthog?.shutdownAsync();
|
||||
|
||||
return { billingPortalUrl: stripeBillingPortalSession.url };
|
||||
}
|
||||
|
|
|
|||
|
|
@ -23,14 +23,14 @@ export const videoRouter = createTRPCRouter({
|
|||
},
|
||||
});
|
||||
|
||||
posthog.capture({
|
||||
posthog?.capture({
|
||||
distinctId: session.user.id,
|
||||
event: "viewing video list",
|
||||
properties: {
|
||||
videoAmount: videos.length,
|
||||
},
|
||||
});
|
||||
void posthog.shutdownAsync();
|
||||
void posthog?.shutdownAsync();
|
||||
|
||||
const videosWithThumbnailUrl = await Promise.all(
|
||||
videos.map(async (video) => {
|
||||
|
|
@ -70,7 +70,7 @@ export const videoRouter = createTRPCRouter({
|
|||
}
|
||||
|
||||
if (session) {
|
||||
posthog.capture({
|
||||
posthog?.capture({
|
||||
distinctId: session.user.id,
|
||||
event: "viewing video",
|
||||
properties: {
|
||||
|
|
@ -83,7 +83,7 @@ export const videoRouter = createTRPCRouter({
|
|||
videoShareLinkExpiresAt: video.shareLinkExpiresAt,
|
||||
},
|
||||
});
|
||||
void posthog.shutdownAsync();
|
||||
void posthog?.shutdownAsync();
|
||||
}
|
||||
|
||||
const getObjectCommand = new GetObjectCommand({
|
||||
|
|
@ -110,7 +110,7 @@ export const videoRouter = createTRPCRouter({
|
|||
videos.length >= 10 &&
|
||||
session.user.stripeSubscriptionStatus !== "active"
|
||||
) {
|
||||
posthog.capture({
|
||||
posthog?.capture({
|
||||
distinctId: session.user.id,
|
||||
event: "hit video upload limit",
|
||||
properties: {
|
||||
|
|
@ -118,7 +118,7 @@ export const videoRouter = createTRPCRouter({
|
|||
stripeSubscriptionStatus: session.user.stripeSubscriptionStatus,
|
||||
},
|
||||
});
|
||||
void posthog.shutdownAsync();
|
||||
void posthog?.shutdownAsync();
|
||||
|
||||
throw new TRPCError({
|
||||
code: "FORBIDDEN",
|
||||
|
|
@ -127,7 +127,7 @@ export const videoRouter = createTRPCRouter({
|
|||
});
|
||||
}
|
||||
|
||||
posthog.capture({
|
||||
posthog?.capture({
|
||||
distinctId: session.user.id,
|
||||
event: "uploading video",
|
||||
properties: {
|
||||
|
|
@ -135,7 +135,7 @@ export const videoRouter = createTRPCRouter({
|
|||
stripeSubscriptionStatus: session.user.stripeSubscriptionStatus,
|
||||
},
|
||||
});
|
||||
void posthog.shutdownAsync();
|
||||
void posthog?.shutdownAsync();
|
||||
|
||||
const video = await prisma.video.create({
|
||||
data: {
|
||||
|
|
@ -184,7 +184,7 @@ export const videoRouter = createTRPCRouter({
|
|||
throw new TRPCError({ code: "FORBIDDEN" });
|
||||
}
|
||||
|
||||
posthog.capture({
|
||||
posthog?.capture({
|
||||
distinctId: session.user.id,
|
||||
event: "update video setSharing",
|
||||
properties: {
|
||||
|
|
@ -192,7 +192,7 @@ export const videoRouter = createTRPCRouter({
|
|||
videoSharing: input.sharing,
|
||||
},
|
||||
});
|
||||
void posthog.shutdownAsync();
|
||||
void posthog?.shutdownAsync();
|
||||
|
||||
return {
|
||||
success: true,
|
||||
|
|
@ -218,7 +218,7 @@ export const videoRouter = createTRPCRouter({
|
|||
throw new TRPCError({ code: "FORBIDDEN" });
|
||||
}
|
||||
|
||||
posthog.capture({
|
||||
posthog?.capture({
|
||||
distinctId: session.user.id,
|
||||
event: "update video delete_after_link_expires",
|
||||
properties: {
|
||||
|
|
@ -226,7 +226,7 @@ export const videoRouter = createTRPCRouter({
|
|||
delete_after_link_expires: input.delete_after_link_expires,
|
||||
},
|
||||
});
|
||||
void posthog.shutdownAsync();
|
||||
void posthog?.shutdownAsync();
|
||||
|
||||
return {
|
||||
success: true,
|
||||
|
|
@ -255,7 +255,7 @@ export const videoRouter = createTRPCRouter({
|
|||
throw new TRPCError({ code: "FORBIDDEN" });
|
||||
}
|
||||
|
||||
posthog.capture({
|
||||
posthog?.capture({
|
||||
distinctId: session.user.id,
|
||||
event: "update video shareLinkExpiresAt",
|
||||
properties: {
|
||||
|
|
@ -263,7 +263,7 @@ export const videoRouter = createTRPCRouter({
|
|||
shareLinkExpiresAt: input.shareLinkExpiresAt,
|
||||
},
|
||||
});
|
||||
void posthog.shutdownAsync();
|
||||
void posthog?.shutdownAsync();
|
||||
|
||||
return {
|
||||
success: true,
|
||||
|
|
@ -292,7 +292,7 @@ export const videoRouter = createTRPCRouter({
|
|||
throw new TRPCError({ code: "FORBIDDEN" });
|
||||
}
|
||||
|
||||
posthog.capture({
|
||||
posthog?.capture({
|
||||
distinctId: session.user.id,
|
||||
event: "update video title",
|
||||
properties: {
|
||||
|
|
@ -300,7 +300,7 @@ export const videoRouter = createTRPCRouter({
|
|||
title: input.title,
|
||||
},
|
||||
});
|
||||
void posthog.shutdownAsync();
|
||||
void posthog?.shutdownAsync();
|
||||
|
||||
return {
|
||||
success: true,
|
||||
|
|
@ -325,14 +325,14 @@ export const videoRouter = createTRPCRouter({
|
|||
throw new TRPCError({ code: "FORBIDDEN" });
|
||||
}
|
||||
|
||||
posthog.capture({
|
||||
posthog?.capture({
|
||||
distinctId: session.user.id,
|
||||
event: "video delete",
|
||||
properties: {
|
||||
videoId: input.videoId,
|
||||
},
|
||||
});
|
||||
void posthog.shutdownAsync();
|
||||
void posthog?.shutdownAsync();
|
||||
|
||||
const deleteVideoObject = await s3.send(
|
||||
new DeleteObjectCommand({
|
||||
|
|
|
|||
|
|
@ -125,9 +125,11 @@ const enforceUserIsAuthed = t.middleware(async ({ ctx, next }) => {
|
|||
if (!ctx.session || !ctx.session.user) {
|
||||
throw new TRPCError({ code: "UNAUTHORIZED" });
|
||||
}
|
||||
const { success } = await rateLimit.limit(ctx.session.user.id);
|
||||
if (!success) {
|
||||
throw new TRPCError({ code: "TOO_MANY_REQUESTS" });
|
||||
if (rateLimit) {
|
||||
const { success } = await rateLimit.limit(ctx.session.user.id);
|
||||
if (!success) {
|
||||
throw new TRPCError({ code: "TOO_MANY_REQUESTS" });
|
||||
}
|
||||
}
|
||||
|
||||
return next({
|
||||
|
|
|
|||
|
|
@ -70,40 +70,44 @@ export const authOptions: NextAuthOptions = {
|
|||
],
|
||||
events: {
|
||||
async signIn(message) {
|
||||
const client = new PostHog(env.NEXT_PUBLIC_POSTHOG_KEY, {
|
||||
host: env.NEXT_PUBLIC_POSTHOG_HOST,
|
||||
});
|
||||
if (!!env.NEXT_PUBLIC_POSTHOG_KEY) {
|
||||
const client = new PostHog(env.NEXT_PUBLIC_POSTHOG_KEY, {
|
||||
host: env.NEXT_PUBLIC_POSTHOG_HOST,
|
||||
});
|
||||
|
||||
client.capture({
|
||||
distinctId: message.user.id,
|
||||
event: "user logged in",
|
||||
properties: {
|
||||
provider: message.account?.provider,
|
||||
isNewUser: message.isNewUser,
|
||||
},
|
||||
});
|
||||
client.capture({
|
||||
distinctId: message.user.id,
|
||||
event: "user logged in",
|
||||
properties: {
|
||||
provider: message.account?.provider,
|
||||
isNewUser: message.isNewUser,
|
||||
},
|
||||
});
|
||||
|
||||
await client.shutdownAsync();
|
||||
await client.shutdownAsync();
|
||||
}
|
||||
},
|
||||
async signOut(message) {
|
||||
const session = message.session as unknown as {
|
||||
id: string;
|
||||
sessionToken: string;
|
||||
userId: string;
|
||||
expires: Date;
|
||||
};
|
||||
if (!session?.userId) return;
|
||||
if (!!env.NEXT_PUBLIC_POSTHOG_KEY) {
|
||||
const session = message.session as unknown as {
|
||||
id: string;
|
||||
sessionToken: string;
|
||||
userId: string;
|
||||
expires: Date;
|
||||
};
|
||||
if (!session?.userId) return;
|
||||
|
||||
const client = new PostHog(env.NEXT_PUBLIC_POSTHOG_KEY, {
|
||||
host: env.NEXT_PUBLIC_POSTHOG_HOST,
|
||||
});
|
||||
const client = new PostHog(env.NEXT_PUBLIC_POSTHOG_KEY, {
|
||||
host: env.NEXT_PUBLIC_POSTHOG_HOST,
|
||||
});
|
||||
|
||||
client.capture({
|
||||
distinctId: session.userId,
|
||||
event: "user logged out",
|
||||
});
|
||||
client.capture({
|
||||
distinctId: session.userId,
|
||||
event: "user logged out",
|
||||
});
|
||||
|
||||
await client.shutdownAsync();
|
||||
await client.shutdownAsync();
|
||||
}
|
||||
},
|
||||
},
|
||||
pages: {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
import { PostHog } from "posthog-node";
|
||||
import { env } from "~/env.mjs";
|
||||
|
||||
export const posthog = new PostHog(env.NEXT_PUBLIC_POSTHOG_KEY, {
|
||||
host: env.NEXT_PUBLIC_POSTHOG_HOST,
|
||||
});
|
||||
export const posthog = !!env.NEXT_PUBLIC_POSTHOG_KEY
|
||||
? new PostHog(env.NEXT_PUBLIC_POSTHOG_KEY, {
|
||||
host: env.NEXT_PUBLIC_POSTHOG_HOST,
|
||||
})
|
||||
: null;
|
||||
|
|
|
|||
|
|
@ -2,10 +2,13 @@ import { Ratelimit } from "@upstash/ratelimit";
|
|||
import { Redis } from "@upstash/redis";
|
||||
import { env } from "~/env.mjs";
|
||||
|
||||
export const rateLimit = new Ratelimit({
|
||||
redis: new Redis({
|
||||
url: env.UPSTASH_REDIS_REST_URL,
|
||||
token: env.UPSTASH_REDIS_REST_TOKEN,
|
||||
}),
|
||||
limiter: Ratelimit.slidingWindow(60, "60 s"),
|
||||
});
|
||||
export const rateLimit =
|
||||
!!env.UPSTASH_REDIS_REST_URL && !!env.UPSTASH_REDIS_REST_TOKEN
|
||||
? new Ratelimit({
|
||||
redis: new Redis({
|
||||
url: env.UPSTASH_REDIS_REST_URL,
|
||||
token: env.UPSTASH_REDIS_REST_TOKEN,
|
||||
}),
|
||||
limiter: Ratelimit.slidingWindow(60, "60 s"),
|
||||
})
|
||||
: null;
|
||||
|
|
|
|||
|
|
@ -60,7 +60,7 @@ export const handleInvoicePaid = async ({
|
|||
event: Stripe.Event;
|
||||
stripe: Stripe;
|
||||
prisma: PrismaClient;
|
||||
posthog: PostHog;
|
||||
posthog: PostHog | null;
|
||||
}) => {
|
||||
const invoice = event.data.object as Stripe.Invoice;
|
||||
const subscriptionId = invoice.subscription;
|
||||
|
|
@ -81,14 +81,14 @@ export const handleInvoicePaid = async ({
|
|||
});
|
||||
|
||||
if (userId && subscription.status) {
|
||||
posthog.capture({
|
||||
posthog?.capture({
|
||||
distinctId: userId,
|
||||
event: "stripe invoice.paid",
|
||||
properties: {
|
||||
stripeSubscriptionStatus: subscription.status,
|
||||
},
|
||||
});
|
||||
void posthog.shutdownAsync();
|
||||
void posthog?.shutdownAsync();
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -99,7 +99,7 @@ export const handleSubscriptionCreatedOrUpdated = async ({
|
|||
}: {
|
||||
event: Stripe.Event;
|
||||
prisma: PrismaClient;
|
||||
posthog: PostHog;
|
||||
posthog: PostHog | null;
|
||||
}) => {
|
||||
const subscription = event.data.object as Stripe.Subscription;
|
||||
const userId = subscription.metadata.userId;
|
||||
|
|
@ -116,11 +116,11 @@ export const handleSubscriptionCreatedOrUpdated = async ({
|
|||
});
|
||||
|
||||
if (userId && subscription.status) {
|
||||
posthog.capture({
|
||||
posthog?.capture({
|
||||
distinctId: userId,
|
||||
event: "stripe subscription created or updated",
|
||||
});
|
||||
void posthog.shutdownAsync();
|
||||
void posthog?.shutdownAsync();
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -131,7 +131,7 @@ export const handleSubscriptionCanceled = async ({
|
|||
}: {
|
||||
event: Stripe.Event;
|
||||
prisma: PrismaClient;
|
||||
posthog: PostHog;
|
||||
posthog: PostHog | null;
|
||||
}) => {
|
||||
const subscription = event.data.object as Stripe.Subscription;
|
||||
const userId = subscription.metadata.userId;
|
||||
|
|
@ -148,10 +148,10 @@ export const handleSubscriptionCanceled = async ({
|
|||
});
|
||||
|
||||
if (userId && subscription.status) {
|
||||
posthog.capture({
|
||||
posthog?.capture({
|
||||
distinctId: userId,
|
||||
event: "stripe subscription cancelled",
|
||||
});
|
||||
void posthog.shutdownAsync();
|
||||
void posthog?.shutdownAsync();
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,5 +1,8 @@
|
|||
import Stripe from "stripe";
|
||||
import { env } from "~/env.mjs";
|
||||
export const stripe = new Stripe(env.STRIPE_SECRET_KEY, {
|
||||
apiVersion: "2022-11-15",
|
||||
});
|
||||
|
||||
export const stripe = !!env.STRIPE_SECRET_KEY
|
||||
? new Stripe(env.STRIPE_SECRET_KEY, {
|
||||
apiVersion: "2022-11-15",
|
||||
})
|
||||
: null;
|
||||
|
|
|
|||
Loading…
Reference in a new issue