mirror of
https://github.com/documenso/documenso.git
synced 2025-11-13 00:03:33 +10:00
fix: tidy stripe feature and add provider
This commit is contained in:
@ -4,7 +4,10 @@
|
||||
"private": true,
|
||||
"main": "index.ts",
|
||||
"dependencies": {
|
||||
"@documenso/prisma": "*",
|
||||
"@prisma/client": "^4.8.1",
|
||||
"bcryptjs": "^2.4.3",
|
||||
"micro": "^10.0.1",
|
||||
"stripe": "^12.4.0"
|
||||
}
|
||||
}
|
||||
7
packages/lib/stripe/client.ts
Normal file
7
packages/lib/stripe/client.ts
Normal file
@ -0,0 +1,7 @@
|
||||
import Stripe from 'stripe';
|
||||
|
||||
|
||||
export const stripe = new Stripe(process.env.STRIPE_API_KEY!, {
|
||||
apiVersion: "2022-11-15",
|
||||
typescript: true,
|
||||
});
|
||||
@ -1,12 +1,14 @@
|
||||
export const STRIPE_PLANS = [
|
||||
{
|
||||
name: "Community Plan",
|
||||
name: "Community Plan (Monthly)",
|
||||
period: "monthly",
|
||||
priceId: process.env.STRIPE_COMMUNITY_PLAN_MONTHLY_PRICE_ID ?? "",
|
||||
price: 30,
|
||||
priceId: process.env.NEXT_PUBLIC_STRIPE_COMMUNITY_PLAN_MONTHLY_PRICE_ID ?? "",
|
||||
},
|
||||
{
|
||||
name: "Community Plan",
|
||||
name: "Community Plan (Yearly)",
|
||||
period: "yearly",
|
||||
priceId: process.env.STRIPE_COMMUNITY_PLAN_YEARLY_PRICE_ID ?? "",
|
||||
price: 300,
|
||||
priceId: process.env.NEXT_PUBLIC_STRIPE_COMMUNITY_PLAN_YEARLY_PRICE_ID ?? "",
|
||||
},
|
||||
];
|
||||
|
||||
35
packages/lib/stripe/guards/subscriptions.ts
Normal file
35
packages/lib/stripe/guards/subscriptions.ts
Normal file
@ -0,0 +1,35 @@
|
||||
import { GetServerSideProps, GetServerSidePropsContext, NextApiRequest } from "next";
|
||||
import { SubscriptionStatus } from "@prisma/client";
|
||||
import { getToken } from "next-auth/jwt";
|
||||
|
||||
export const isSubscriptionsEnabled = () => {
|
||||
return process.env.NEXT_PUBLIC_ALLOW_SUBSCRIPTIONS === "true";
|
||||
};
|
||||
|
||||
export const isSubscribedServer = async (
|
||||
req: NextApiRequest | GetServerSidePropsContext["req"]
|
||||
) => {
|
||||
const { default: prisma } = await import("@documenso/prisma");
|
||||
|
||||
if (!isSubscriptionsEnabled()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const token = await getToken({
|
||||
req,
|
||||
});
|
||||
|
||||
if (!token || !token.email) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const subscription = await prisma.subscription.findFirst({
|
||||
where: {
|
||||
User: {
|
||||
email: token.email,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
return subscription !== null && subscription.status !== SubscriptionStatus.INACTIVE;
|
||||
};
|
||||
@ -1,11 +1,11 @@
|
||||
import { NextApiRequest, NextApiResponse } from "next";
|
||||
import prisma from "@documenso/prisma";
|
||||
import { stripe } from "../index";
|
||||
import { stripe } from "../client";
|
||||
import { getToken } from "next-auth/jwt";
|
||||
|
||||
export type CheckoutSessionRequest = {
|
||||
body: {
|
||||
id: string;
|
||||
id?: string;
|
||||
priceId: string;
|
||||
};
|
||||
};
|
||||
@ -61,7 +61,7 @@ export const checkoutSessionHandler = async (req: NextApiRequest, res: NextApiRe
|
||||
|
||||
const { id, priceId } = req.body;
|
||||
|
||||
if (typeof id !== "string" || typeof priceId !== "string") {
|
||||
if (typeof priceId !== "string") {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: "No id or priceId found in request",
|
||||
@ -70,6 +70,7 @@ export const checkoutSessionHandler = async (req: NextApiRequest, res: NextApiRe
|
||||
|
||||
const session = await stripe.checkout.sessions.create({
|
||||
customer: id,
|
||||
customer_email: user.email,
|
||||
client_reference_id: String(user.id),
|
||||
payment_method_types: ["card"],
|
||||
line_items: [
|
||||
@ -80,8 +81,8 @@ export const checkoutSessionHandler = async (req: NextApiRequest, res: NextApiRe
|
||||
],
|
||||
mode: "subscription",
|
||||
allow_promotion_codes: true,
|
||||
success_url: `${process.env.NEXT_PUBLIC_BASE_URL}/settings/billing?success=true`,
|
||||
cancel_url: `${process.env.NEXT_PUBLIC_BASE_URL}/settings/billing?canceled=true`,
|
||||
success_url: `${process.env.NEXT_PUBLIC_WEBAPP_URL}/settings/billing?success=true`,
|
||||
cancel_url: `${process.env.NEXT_PUBLIC_WEBAPP_URL}/settings/billing?canceled=true`,
|
||||
});
|
||||
|
||||
return res.status(200).json({
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import { NextApiRequest, NextApiResponse } from "next";
|
||||
import { stripe } from "../index";
|
||||
import { stripe } from "../client";
|
||||
|
||||
|
||||
export type PortalSessionRequest = {
|
||||
body: {
|
||||
@ -43,11 +44,11 @@ export const portalSessionHandler = async (req: NextApiRequest, res: NextApiResp
|
||||
|
||||
const session = await stripe.billingPortal.sessions.create({
|
||||
customer: id,
|
||||
return_url: `${process.env.NEXT_PUBLIC_BASE_URL}/settings/billing`,
|
||||
return_url: `${process.env.NEXT_PUBLIC_WEBAPP_URL}/settings/billing`,
|
||||
});
|
||||
|
||||
return res.status(200).json({
|
||||
success: true,
|
||||
url: session.url,
|
||||
});
|
||||
};
|
||||
};
|
||||
@ -1,8 +1,11 @@
|
||||
import prisma from "@documenso/prisma";
|
||||
import { SubscriptionStatus } from "@prisma/client";
|
||||
import { NextApiRequest, NextApiResponse } from "next";
|
||||
import prisma from "@documenso/prisma";
|
||||
import { stripe } from "../client";
|
||||
import { SubscriptionStatus } from "@prisma/client";
|
||||
import { buffer } from "micro";
|
||||
import Stripe from "stripe";
|
||||
import { stripe } from "../index";
|
||||
|
||||
const log = (...args: any[]) => console.log("[stripe]", ...args);
|
||||
|
||||
export const webhookHandler = async (req: NextApiRequest, res: NextApiResponse) => {
|
||||
if (!process.env.NEXT_PUBLIC_ALLOW_SUBSCRIPTIONS) {
|
||||
@ -22,7 +25,12 @@ export const webhookHandler = async (req: NextApiRequest, res: NextApiResponse)
|
||||
});
|
||||
}
|
||||
|
||||
const event = stripe.webhooks.constructEvent(req.body, sig, process.env.STRIPE_WEBHOOK_SECRET!);
|
||||
log("constructing body...")
|
||||
const body = await buffer(req);
|
||||
log("constructed body")
|
||||
|
||||
const event = stripe.webhooks.constructEvent(body, sig, process.env.STRIPE_WEBHOOK_SECRET!);
|
||||
log("event-type:", event.type);
|
||||
|
||||
if (event.type === "checkout.session.completed") {
|
||||
const session = event.data.object as Stripe.Checkout.Session;
|
||||
@ -120,7 +128,7 @@ export const webhookHandler = async (req: NextApiRequest, res: NextApiResponse)
|
||||
priceId: updatedSubscription.items.data[0].price.id,
|
||||
periodEnd: new Date(updatedSubscription.current_period_end * 1000),
|
||||
},
|
||||
})
|
||||
});
|
||||
|
||||
return res.status(200).json({
|
||||
success: true,
|
||||
@ -148,6 +156,7 @@ export const webhookHandler = async (req: NextApiRequest, res: NextApiResponse)
|
||||
});
|
||||
}
|
||||
|
||||
log("Unhandled webhook event", event.type);
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: "Unhandled webhook event",
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import Stripe from 'stripe';
|
||||
|
||||
export const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!, {
|
||||
apiVersion: "2022-11-15",
|
||||
typescript: true,
|
||||
});
|
||||
export * from './data/plans'
|
||||
export * from './fetchers/checkout-session'
|
||||
export * from './fetchers/get-subscription'
|
||||
export * from './fetchers/portal-session'
|
||||
export * from './guards/subscriptions'
|
||||
export * from './providers/subscription-provider'
|
||||
|
||||
89
packages/lib/stripe/providers/subscription-provider.tsx
Normal file
89
packages/lib/stripe/providers/subscription-provider.tsx
Normal file
@ -0,0 +1,89 @@
|
||||
import React, { createContext, useContext, useEffect, useMemo, useState } from "react";
|
||||
import { fetchSubscription } from "../fetchers/get-subscription";
|
||||
import { Subscription, SubscriptionStatus } from "@prisma/client";
|
||||
import { useSession } from "next-auth/react";
|
||||
|
||||
|
||||
export type SubscriptionContextValue = {
|
||||
subscription: Subscription | null;
|
||||
hasSubscription: boolean;
|
||||
isLoading: boolean;
|
||||
};
|
||||
|
||||
const SubscriptionContext = createContext<SubscriptionContextValue>({
|
||||
subscription: null,
|
||||
hasSubscription: false,
|
||||
isLoading: false,
|
||||
});
|
||||
|
||||
export const useSubscription = () => {
|
||||
const context = useContext(SubscriptionContext);
|
||||
|
||||
if (!context) {
|
||||
throw new Error(`useSubscription must be used within a SubscriptionProvider`);
|
||||
}
|
||||
|
||||
return context;
|
||||
};
|
||||
|
||||
export interface SubscriptionProviderProps {
|
||||
children: React.ReactNode;
|
||||
initialSubscription?: Subscription;
|
||||
}
|
||||
|
||||
export const SubscriptionProvider = ({
|
||||
children,
|
||||
initialSubscription,
|
||||
}: SubscriptionProviderProps) => {
|
||||
const session = useSession();
|
||||
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [subscription, setSubscription] = useState<Subscription | null>(
|
||||
initialSubscription || null
|
||||
);
|
||||
|
||||
const hasSubscription = useMemo(() => {
|
||||
console.log({
|
||||
"process.env.NEXT_PUBLIC_ALLOW_SUBSCRIPTIONS": process.env.NEXT_PUBLIC_ALLOW_SUBSCRIPTIONS,
|
||||
enabled: process.env.NEXT_PUBLIC_ALLOW_SUBSCRIPTIONS === "true",
|
||||
"subscription.status": subscription?.status,
|
||||
"subscription.periodEnd": subscription?.periodEnd,
|
||||
});
|
||||
|
||||
if (process.env.NEXT_PUBLIC_ALLOW_SUBSCRIPTIONS === "true") {
|
||||
return (
|
||||
subscription?.status === SubscriptionStatus.ACTIVE &&
|
||||
!!subscription?.periodEnd &&
|
||||
new Date(subscription.periodEnd) > new Date()
|
||||
);
|
||||
}
|
||||
|
||||
return true;
|
||||
}, [subscription]);
|
||||
|
||||
useEffect(() => {
|
||||
if (process.env.NEXT_PUBLIC_ALLOW_SUBSCRIPTIONS === "true" && session.data) {
|
||||
setIsLoading(true);
|
||||
fetchSubscription().then((res) => {
|
||||
if (res.success) {
|
||||
setSubscription(res.subscription);
|
||||
} else {
|
||||
setSubscription(null);
|
||||
}
|
||||
|
||||
setIsLoading(false);
|
||||
});
|
||||
}
|
||||
}, [session.data]);
|
||||
|
||||
return (
|
||||
<SubscriptionContext.Provider
|
||||
value={{
|
||||
subscription,
|
||||
hasSubscription,
|
||||
isLoading,
|
||||
}}>
|
||||
{children}
|
||||
</SubscriptionContext.Provider>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user