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,
|
"private": true,
|
||||||
"main": "index.ts",
|
"main": "index.ts",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@documenso/prisma": "*",
|
||||||
|
"@prisma/client": "^4.8.1",
|
||||||
"bcryptjs": "^2.4.3",
|
"bcryptjs": "^2.4.3",
|
||||||
|
"micro": "^10.0.1",
|
||||||
"stripe": "^12.4.0"
|
"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 = [
|
export const STRIPE_PLANS = [
|
||||||
{
|
{
|
||||||
name: "Community Plan",
|
name: "Community Plan (Monthly)",
|
||||||
period: "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",
|
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 { NextApiRequest, NextApiResponse } from "next";
|
||||||
import prisma from "@documenso/prisma";
|
import prisma from "@documenso/prisma";
|
||||||
import { stripe } from "../index";
|
import { stripe } from "../client";
|
||||||
import { getToken } from "next-auth/jwt";
|
import { getToken } from "next-auth/jwt";
|
||||||
|
|
||||||
export type CheckoutSessionRequest = {
|
export type CheckoutSessionRequest = {
|
||||||
body: {
|
body: {
|
||||||
id: string;
|
id?: string;
|
||||||
priceId: string;
|
priceId: string;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
@ -61,7 +61,7 @@ export const checkoutSessionHandler = async (req: NextApiRequest, res: NextApiRe
|
|||||||
|
|
||||||
const { id, priceId } = req.body;
|
const { id, priceId } = req.body;
|
||||||
|
|
||||||
if (typeof id !== "string" || typeof priceId !== "string") {
|
if (typeof priceId !== "string") {
|
||||||
return res.status(400).json({
|
return res.status(400).json({
|
||||||
success: false,
|
success: false,
|
||||||
message: "No id or priceId found in request",
|
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({
|
const session = await stripe.checkout.sessions.create({
|
||||||
customer: id,
|
customer: id,
|
||||||
|
customer_email: user.email,
|
||||||
client_reference_id: String(user.id),
|
client_reference_id: String(user.id),
|
||||||
payment_method_types: ["card"],
|
payment_method_types: ["card"],
|
||||||
line_items: [
|
line_items: [
|
||||||
@ -80,8 +81,8 @@ export const checkoutSessionHandler = async (req: NextApiRequest, res: NextApiRe
|
|||||||
],
|
],
|
||||||
mode: "subscription",
|
mode: "subscription",
|
||||||
allow_promotion_codes: true,
|
allow_promotion_codes: true,
|
||||||
success_url: `${process.env.NEXT_PUBLIC_BASE_URL}/settings/billing?success=true`,
|
success_url: `${process.env.NEXT_PUBLIC_WEBAPP_URL}/settings/billing?success=true`,
|
||||||
cancel_url: `${process.env.NEXT_PUBLIC_BASE_URL}/settings/billing?canceled=true`,
|
cancel_url: `${process.env.NEXT_PUBLIC_WEBAPP_URL}/settings/billing?canceled=true`,
|
||||||
});
|
});
|
||||||
|
|
||||||
return res.status(200).json({
|
return res.status(200).json({
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
import { NextApiRequest, NextApiResponse } from "next";
|
import { NextApiRequest, NextApiResponse } from "next";
|
||||||
import { stripe } from "../index";
|
import { stripe } from "../client";
|
||||||
|
|
||||||
|
|
||||||
export type PortalSessionRequest = {
|
export type PortalSessionRequest = {
|
||||||
body: {
|
body: {
|
||||||
@ -43,7 +44,7 @@ export const portalSessionHandler = async (req: NextApiRequest, res: NextApiResp
|
|||||||
|
|
||||||
const session = await stripe.billingPortal.sessions.create({
|
const session = await stripe.billingPortal.sessions.create({
|
||||||
customer: id,
|
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({
|
return res.status(200).json({
|
||||||
|
|||||||
@ -1,8 +1,11 @@
|
|||||||
import prisma from "@documenso/prisma";
|
|
||||||
import { SubscriptionStatus } from "@prisma/client";
|
|
||||||
import { NextApiRequest, NextApiResponse } from "next";
|
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 "stripe";
|
||||||
import { stripe } from "../index";
|
|
||||||
|
const log = (...args: any[]) => console.log("[stripe]", ...args);
|
||||||
|
|
||||||
export const webhookHandler = async (req: NextApiRequest, res: NextApiResponse) => {
|
export const webhookHandler = async (req: NextApiRequest, res: NextApiResponse) => {
|
||||||
if (!process.env.NEXT_PUBLIC_ALLOW_SUBSCRIPTIONS) {
|
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") {
|
if (event.type === "checkout.session.completed") {
|
||||||
const session = event.data.object as Stripe.Checkout.Session;
|
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,
|
priceId: updatedSubscription.items.data[0].price.id,
|
||||||
periodEnd: new Date(updatedSubscription.current_period_end * 1000),
|
periodEnd: new Date(updatedSubscription.current_period_end * 1000),
|
||||||
},
|
},
|
||||||
})
|
});
|
||||||
|
|
||||||
return res.status(200).json({
|
return res.status(200).json({
|
||||||
success: true,
|
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({
|
return res.status(400).json({
|
||||||
success: false,
|
success: false,
|
||||||
message: "Unhandled webhook event",
|
message: "Unhandled webhook event",
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import Stripe from 'stripe';
|
export * from './data/plans'
|
||||||
|
export * from './fetchers/checkout-session'
|
||||||
export const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!, {
|
export * from './fetchers/get-subscription'
|
||||||
apiVersion: "2022-11-15",
|
export * from './fetchers/portal-session'
|
||||||
typescript: true,
|
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