Ensure stripe is not a requirement in our site anymore

This commit is contained in:
Luis Rojas
2024-08-14 16:17:34 -04:00
parent fac168043c
commit c7840ca5b9
18 changed files with 104 additions and 38 deletions
+1 -1
View File
@@ -112,7 +112,7 @@ STRIPE_SECRET_KEY=thisisnotakey
STRIPE_WEBHOOK_SECRET=thisisnotasecret
STRIPE_CONNECT_WEBHOOK_SECRET=thisisnotasecret
STRIPE_DONATE_ID=price_1MZHyDLAn4if8jivVbH5PhMc
STRIPE_METADATA_KEY=tier
TIER_METADATA_KEY=tier
# Features
FEATURE_FLAG_EARLY_ACCESS_MODEL=public
+19 -11
View File
@@ -163,7 +163,6 @@ model User {
isModerator Boolean? @default(false)
createdAt DateTime @default(now())
deletedAt DateTime?
customerId String? @unique
subscriptionId String?
subscription CustomerSubscription?
mutedAt DateTime? /// Updated via trigger
@@ -178,6 +177,8 @@ model User {
excludeFromLeaderboards Boolean @default(false)
rewardsEligibility RewardsEligibility @default(Eligible)
eligibilityChangedAt DateTime?
// Payment provider related
customerId String? @unique
paddleCustomerId String? @unique
profile UserProfile?
@@ -293,13 +294,19 @@ model CustomerSubscription {
updatedAt DateTime?
}
enum PaymentProvider {
Stripe
Paddle
}
model Product {
id String @id
id String @id
active Boolean
name String
description String?
metadata Json
defaultPriceId String?
provider PaymentProvider @default(Stripe)
prices Price[]
customerSubscriptions CustomerSubscription[]
@@ -320,18 +327,19 @@ model Price {
metadata Json
customerSubscriptions CustomerSubscription[]
purchases Purchase[]
provider PaymentProvider @default(Stripe)
}
model Purchase {
id Int @id @default(autoincrement())
customerId String
customer User @relation(fields: [customerId], references: [customerId])
productId String?
product Product? @relation(fields: [productId], references: [id])
priceId String?
price Price? @relation(fields: [priceId], references: [id])
status String?
createdAt DateTime @default(now())
id Int @id @default(autoincrement())
userId Int
customer User @relation(fields: [userId], references: [id])
productId String?
product Product? @relation(fields: [productId], references: [id])
priceId String?
price Price? @relation(fields: [priceId], references: [id])
status String?
createdAt DateTime @default(now())
}
enum UserEngagementType {
+4
View File
@@ -23,6 +23,10 @@ export const useQueryBuzzPackages = ({ onPurchaseSuccess }: { onPurchaseSuccess?
if (url) await router.push(url);
else {
const stripe = await getClientStripe();
if (!stripe) {
return;
}
await stripe.redirectToCheckout({ sessionId });
}
},
+3
View File
@@ -10,6 +10,9 @@ export function DonateButton({ children }: { children: React.ReactElement }) {
if (url) Router.push(url);
else {
const stripe = await getClientStripe();
if (!stripe) {
return;
}
await stripe.redirectToCheckout({ sessionId });
}
},
@@ -31,6 +31,9 @@ export function SubscribeButton({
if (url) Router.push(url);
else if (sessionId) {
const stripe = await getClientStripe();
if (!stripe) {
return;
}
await stripe.redirectToCheckout({ sessionId });
}
},
+10 -9
View File
@@ -84,11 +84,6 @@ export const serverSchema = z.object({
UNAUTHENTICATED_DOWNLOAD: zc.booleanString,
UNAUTHENTICATED_LIST_NSFW: zc.booleanString,
SHOW_SFW_IN_NSFW: zc.booleanString,
STRIPE_SECRET_KEY: z.string(),
STRIPE_WEBHOOK_SECRET: z.string(),
STRIPE_CONNECT_WEBHOOK_SECRET: z.string(),
STRIPE_DONATE_ID: z.string(),
STRIPE_METADATA_KEY: z.string(),
LOGGING: commaDelimitedStringArray(),
IMAGE_SCANNING_ENDPOINT: z.string().optional(),
IMAGE_SCANNING_CALLBACK: z.string().optional(),
@@ -172,8 +167,14 @@ export const serverSchema = z.object({
UPLOAD_PROHIBITED_EXTENSIONS: commaDelimitedStringArray().optional(),
POST_INTENT_DETAILS_HOSTS: z.preprocess(stringToArray, z.array(z.string().url()).optional()),
CHOPPED_TOKEN: z.string().optional(),
PADDLE_SECRET_KEY: z.string(),
PADDLE_WEBHOOK_SECRET: z.string(),
PAYMENT_PROCESSOR: z.enum(['Stripe', 'Paddle']).default('Stripe'),
TIER_METADATA_KEY: z.string().default('tier'),
STRIPE_SECRET_KEY: z.string().optional(),
STRIPE_WEBHOOK_SECRET: z.string().optional(),
STRIPE_CONNECT_WEBHOOK_SECRET: z.string().optional(),
STRIPE_DONATE_ID: z.string().optional(),
PADDLE_SECRET_KEY: z.string().optional(),
PADDLE_WEBHOOK_SECRET: z.string().optional(),
});
/**
@@ -182,7 +183,6 @@ export const serverSchema = z.object({
* To expose them to the client, prefix them with `NEXT_PUBLIC_`.
*/
export const clientSchema = z.object({
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY: z.string(),
NEXT_PUBLIC_CONTENT_DECTECTION_LOCATION: z.string(),
NEXT_PUBLIC_IMAGE_LOCATION: z.string(),
NEXT_PUBLIC_CIVITAI_LINK: z.string().url(),
@@ -212,6 +212,7 @@ export const clientSchema = z.object({
NEXT_PUBLIC_PAYPAL_CLIENT_ID: z.string().optional(),
NEXT_PUBLIC_CHOPPED_ENDPOINT: z.string().url().optional(),
NEXT_PUBLIC_PADDLE_TOKEN: z.string().optional(),
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY: z.string().optional(),
});
/**
@@ -221,7 +222,6 @@ export const clientSchema = z.object({
* @type {{ [k in keyof z.infer<typeof clientSchema>]: z.infer<typeof clientSchema>[k] | undefined }}
*/
export const clientEnv = {
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY: process.env.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY,
NEXT_PUBLIC_CONTENT_DECTECTION_LOCATION: process.env.NEXT_PUBLIC_CONTENT_DECTECTION_LOCATION,
NEXT_PUBLIC_IMAGE_LOCATION: process.env.NEXT_PUBLIC_IMAGE_LOCATION,
NEXT_PUBLIC_GIT_HASH: process.env.NEXT_PUBLIC_GIT_HASH,
@@ -250,5 +250,6 @@ export const clientEnv = {
NEXT_PUBLIC_ADS: process.env.NEXT_PUBLIC_ADS === 'true',
NEXT_PUBLIC_PAYPAL_CLIENT_ID: process.env.NEXT_PUBLIC_PAYPAL_CLIENT_ID,
NEXT_PUBLIC_CHOPPED_ENDPOINT: process.env.NEXT_PUBLIC_CHOPPED_ENDPOINT,
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY: process.env.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY,
NEXT_PUBLIC_PADDLE_TOKEN: process.env.NEXT_PUBLIC_PADDLE_TOKEN,
};
+1 -1
View File
@@ -29,7 +29,7 @@ export default WebhookEndpoint(async function (req: NextApiRequest, res: NextApi
u.email,
(
SELECT
p.metadata->>'${env.STRIPE_METADATA_KEY}'
p.metadata->>'${env.TIER_METADATA_KEY}'
FROM "CustomerSubscription" s
JOIN "Product" p ON p.id = s."productId"
WHERE s."userId" = u.id AND s.status IN ('active', 'trialing')
+3
View File
@@ -26,6 +26,9 @@ const relevantEvents = new Set(['account.updated', 'transfer.created']);
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
if (req.method === 'POST') {
const stripe = await getServerStripe();
if (!stripe) {
return;
}
const buf = await buffer(req);
console.log(req.headers, req.env);
+3
View File
@@ -52,6 +52,9 @@ const relevantEvents = new Set([
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
if (req.method === 'POST') {
const stripe = await getServerStripe();
if (!stripe) {
return;
}
const buf = await buffer(req);
const sig = req.headers['stripe-signature'];
+3 -3
View File
@@ -1,15 +1,15 @@
import { Elements } from '@stripe/react-stripe-js';
import { loadStripe, Stripe } from '@stripe/stripe-js';
import { useRef, useEffect } from 'react';
import { env } from '~/env/client.mjs';
import { useRouter } from 'next/router';
import { z } from 'zod';
import { commaDelimitedStringArray } from '~/utils/zod-helpers';
import { showSuccessNotification } from '~/utils/notifications';
import { Anchor, Stack, Text } from '@mantine/core';
import { removeEmpty } from '~/utils/object-helpers';
const stripePromise = loadStripe(env.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY);
const stripePromise = env.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY
? loadStripe(env.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY)
: null;
export const useStripePromise = () => {
const ref = useRef<Promise<Stripe | null> | null>(null);
@@ -169,6 +169,11 @@ export const processClubMembershipRecurringPayments = createJob(
return;
}
// TODO.PaddleIntegration: Check for active payment provider and use that instead of stripe.
if (!stripe) {
return;
}
const paymentMethods = await stripe.paymentMethods.list({
customer: user.customerId as string,
// type: 'card',
+4
View File
@@ -356,6 +356,10 @@ export async function completeStripeBuzzTransaction({
}> {
try {
const stripe = await getServerStripe();
if (!stripe) {
throw throwBadRequestError('Stripe not available');
}
const paymentIntent = await stripe.paymentIntents.retrieve(stripePaymentIntentId, {
expand: ['payment_method'],
});
@@ -315,6 +315,7 @@ export const completeClubMembershipCharge = async ({
stripePaymentIntentId: string;
}) => {
const stripe = await getServerStripe();
if (!stripe) throw throwBadRequestError('Stripe is not available');
const paymentIntent = await stripe.paymentIntents.retrieve(stripePaymentIntentId, {
expand: ['payment_method'],
});
+29 -6
View File
@@ -61,7 +61,7 @@ export const getPlans = async () => {
// Only show the default price for a subscription product
return products
.filter(({ metadata }) => {
return !!(metadata as any)?.[env.STRIPE_METADATA_KEY];
return env.TIER_METADATA_KEY ? !!(metadata as any)?.[env.TIER_METADATA_KEY] : true;
})
.map((product) => {
const prices = product.prices.map((x) => ({ ...x, unitAmount: x.unitAmount ?? 0 }));
@@ -126,6 +126,7 @@ export type StripeSubscription = Awaited<ReturnType<typeof getUserSubscription>>
export const createCustomer = async ({ id, email }: Schema.CreateCustomerInput) => {
const stripe = await getServerStripe();
if (!stripe) throw throwBadRequestError('Stripe is not available');
const user = await dbWrite.user.findUnique({ where: { id }, select: { customerId: true } });
if (!user?.customerId) {
@@ -149,6 +150,7 @@ export const createSubscribeSession = async ({
user: Schema.CreateCustomerInput;
}) => {
const stripe = await getServerStripe();
if (!stripe) throw throwBadRequestError('Stripe is not available');
if (!customerId) {
customerId = await createCustomer(user);
@@ -156,7 +158,7 @@ export const createSubscribeSession = async ({
const products = await dbRead.product.findMany({});
const membershipProducts = products.filter(({ metadata }) => {
return !!(metadata as any)?.[env.STRIPE_METADATA_KEY];
return !!(metadata as any)?.[env.TIER_METADATA_KEY];
});
// Check to see if this user has a subscription with Stripe
@@ -300,6 +302,7 @@ export const createSubscribeSession = async ({
// export const createPortalSession = async ({ customerId }: { customerId: string }) => {
// const stripe = await getServerStripe();
// if (!stripe) throw throwBadRequestError('Stripe is not available');
// const session = await stripe.billingPortal.sessions.create({
// customer: customerId,
// return_url: `${baseUrl}/pricing`,
@@ -318,6 +321,7 @@ export const createDonateSession = async ({
returnUrl: string;
}) => {
const stripe = await getServerStripe();
if (!stripe) throw throwBadRequestError('Stripe is not available');
if (!customerId) {
customerId = await createCustomer(user);
@@ -337,6 +341,7 @@ export const createDonateSession = async ({
export const createManageSubscriptionSession = async ({ customerId }: { customerId: string }) => {
const stripe = await getServerStripe();
if (!stripe) throw throwBadRequestError('Stripe is not available');
const session = await stripe.billingPortal.sessions.create({
customer: customerId,
@@ -358,6 +363,7 @@ export const createSubscriptionChangeSession = async ({
priceId: string;
}) => {
const stripe = await getServerStripe();
if (!stripe) throw throwBadRequestError('Stripe is not available');
const session = await stripe.billingPortal.sessions.create({
customer: customerId,
@@ -389,6 +395,7 @@ export const createSubscriptionChangeSession = async ({
export const createCancelSubscriptionSession = async ({ customerId }: { customerId: string }) => {
const stripe = await getServerStripe();
if (!stripe) throw throwBadRequestError('Stripe is not available');
// Check to see if this user has a subscription with Stripe
const { data: subscriptions } = await stripe.subscriptions.list({
@@ -426,6 +433,7 @@ export const createBuzzSession = async ({
user: Schema.CreateCustomerInput;
}) => {
const stripe = await getServerStripe();
if (!stripe) throw throwBadRequestError('Stripe is not available');
if (!customerId) {
customerId = await createCustomer(user);
@@ -468,6 +476,7 @@ export const upsertSubscription = async (
eventType: string
) => {
const stripe = await getServerStripe();
if (!stripe) throw throwBadRequestError('Stripe is not available');
const isUpdatingSubscription = eventType === 'customer.subscription.updated';
if (isUpdatingSubscription) {
@@ -630,6 +639,7 @@ export const upsertPriceRecord = async (price: Stripe.Price) => {
export const initStripePrices = async () => {
const stripe = await getServerStripe();
if (!stripe) throw throwBadRequestError('Stripe is not available');
const { data: prices } = await stripe.prices.list();
await Promise.all(
prices.map(async (price) => {
@@ -640,6 +650,7 @@ export const initStripePrices = async () => {
export const initStripeProducts = async () => {
const stripe = await getServerStripe();
if (!stripe) throw throwBadRequestError('Stripe is not available');
const { data: products } = await stripe.products.list();
await Promise.all(
products.map(async (product) => {
@@ -650,13 +661,18 @@ export const initStripeProducts = async () => {
export const manageCheckoutPayment = async (sessionId: string, customerId: string) => {
const stripe = await getServerStripe();
if (!stripe) throw throwBadRequestError('Stripe is not available');
const user = await dbWrite.user.findUniqueOrThrow({
where: { customerId },
select: { id: true, customerId: true },
});
const { line_items, payment_status } = await stripe.checkout.sessions.retrieve(sessionId, {
expand: ['line_items'],
});
const purchases =
line_items?.data.map((data) => ({
customerId,
userId: user.id,
priceId: data.price?.id,
productId: data.price?.product as string | undefined,
status: payment_status,
@@ -670,7 +686,7 @@ export const manageCheckoutPayment = async (sessionId: string, customerId: strin
export const manageInvoicePaid = async (invoice: Stripe.Invoice) => {
// Check if user exists and has a customerId
// Use write db to avoid replication lag between webhook requests
const user = await dbWrite.user.findUnique({
const user = await dbWrite.user.findUniqueOrThrow({
where: { customerId: invoice.customer as string },
select: { id: true, customerId: true },
});
@@ -685,7 +701,7 @@ export const manageInvoicePaid = async (invoice: Stripe.Invoice) => {
}
const purchases = invoice.lines.data.map((data) => ({
customerId: invoice.customer as string,
userId: user.id,
priceId: data.price?.id,
productId: data.price?.product as string | undefined,
status: invoice.status,
@@ -703,7 +719,7 @@ export const manageInvoicePaid = async (invoice: Stripe.Invoice) => {
)
) {
const products = (await dbRead.product.findMany()).filter(
(p) => !!(p.metadata as any)?.[env.STRIPE_METADATA_KEY]
(p) => !!(p.metadata as any)?.[env.TIER_METADATA_KEY]
);
const billedProduct = products.find((p) =>
invoice.lines.data.some((l) => l.price?.product === p.id)
@@ -753,6 +769,8 @@ export const cancelSubscription = async ({
if (!subscriptionId) return;
const stripe = await getServerStripe();
if (!stripe) throw throwBadRequestError('Stripe is not available');
await stripe.subscriptions.del(subscriptionId);
};
@@ -821,6 +839,7 @@ export const getPaymentIntent = async ({
}
const stripe = await getServerStripe();
if (!stripe) throw throwBadRequestError('Stripe is not available');
const paymentIntent = await stripe.paymentIntents.create({
amount: unitAmount,
currency,
@@ -866,6 +885,7 @@ export const getPaymentIntentsForBuzz = async ({
const unixEndingAt = endingAt ? Math.floor(endingAt.getTime() / 1000) : undefined;
const stripe = await getServerStripe();
if (!stripe) throw throwBadRequestError('Stripe is not available');
const paymentIntents = await stripe.paymentIntents.list({
customer,
limit: 100, // max limit is 100
@@ -920,6 +940,7 @@ export const getSetupIntent = async ({
}
const stripe = await getServerStripe();
if (!stripe) throw throwBadRequestError('Stripe is not available');
const setupIntent = await stripe.setupIntents.create({
automatic_payment_methods: !paymentMethodTypes
? {
@@ -937,6 +958,7 @@ export const getSetupIntent = async ({
export const getCustomerPaymentMethods = async (customerId: string) => {
const stripe = await getServerStripe();
if (!stripe) throw throwBadRequestError('Stripe is not available');
const paymentMethods = await stripe.paymentMethods.list({
customer: customerId,
});
@@ -953,6 +975,7 @@ export const deleteCustomerPaymentMethod = async ({
isModerator: boolean;
}) => {
const stripe = await getServerStripe();
if (!stripe) throw throwBadRequestError('Stripe is not available');
const paymentMethod = await stripe.paymentMethods.retrieve(paymentMethodId);
if (!paymentMethod) {
@@ -20,6 +20,7 @@ export async function getUserStripeConnectAccount({ userId }: { userId: number }
export async function createUserStripeConnectAccount({ userId }: { userId: number }) {
const stripe = await getServerStripe();
if (!stripe) throw throwBadRequestError('Stripe not available');
const user = await dbRead.user.findUnique({ where: { id: userId } });
if (!user) throw throwBadRequestError(`User not found: ${userId}`);
@@ -68,6 +69,8 @@ export async function getStripeConnectOnboardingLink({ userId }: { userId: numbe
if (!userStripeConnect) throw throwBadRequestError('User stripe connect account not found');
const stripe = await getServerStripe();
if (!stripe) throw throwBadRequestError('Stripe not available');
const accountLink = await stripe.accountLinks.create({
account: userStripeConnect.connectedAccountId,
refresh_url: `${env.NEXT_PUBLIC_BASE_URL}/user/stripe-connect/onboard`,
@@ -163,6 +166,8 @@ export const payToStripeConnectAccount = async ({
metadata?: MixedObject;
}) => {
const stripe = await getServerStripe();
if (!stripe) throw throwBadRequestError('Stripe not available');
const toUserStripeConnect = await getUserStripeConnectAccount({ userId: toUserId });
if (!toUserStripeConnect) throw throwBadRequestError('User stripe connect account not found');
@@ -188,6 +193,7 @@ export const payToStripeConnectAccount = async ({
export const revertStripeConnectTransfer = async ({ transferId }: { transferId: string }) => {
const stripe = await getServerStripe();
if (!stripe) throw throwBadRequestError('Stripe not available');
try {
const transfer = await stripe.transfers.retrieve(transferId, {
+1 -1
View File
@@ -674,7 +674,7 @@ export const getSessionUser = async ({ userId, token }: { userId?: number; token
const { subscription, profilePicture, profilePictureId, settings, ...rest } = user;
const tier: UserTier | undefined =
subscription && ['active', 'trialing'].includes(subscription.status)
? (subscription.product.metadata as any)[env.STRIPE_METADATA_KEY]
? (subscription.product.metadata as any)[env.TIER_METADATA_KEY]
: undefined;
const memberInBadState =
(subscription &&
+4
View File
@@ -3,6 +3,10 @@ import { env } from '~/env/server.mjs';
let stripe: Stripe;
export const getServerStripe = async () => {
if (!env.STRIPE_SECRET_KEY) {
return;
}
if (!stripe)
stripe = await new Stripe(env.STRIPE_SECRET_KEY, {
typescript: true,
+4 -6
View File
@@ -1,12 +1,10 @@
import { env } from '~/env/client.mjs';
import { Stripe, loadStripe } from '@stripe/stripe-js';
// let stripePromise: Promise<Stripe | null>;
export const getClientStripe = () => {
if (!env.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY) {
return null;
}
return loadStripe(env.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY) as Promise<Stripe>;
// if (!stripePromise) {
// console.log('client', env.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY);
// stripePromise = loadStripe(env.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY);
// }
// return stripePromise as Promise<Stripe>;
};