fix: tidy stripe feature and add provider

This commit is contained in:
Mythie
2023-05-06 14:33:27 +10:00
parent 900b816ae0
commit da0166b746
9 changed files with 170 additions and 23 deletions

View File

@ -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"
}
}

View 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,
});

View File

@ -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 ?? "",
},
];

View 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;
};

View File

@ -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({

View File

@ -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,
});
};
};

View File

@ -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",

View File

@ -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'

View 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>
);
};