Skip to content

Commit

Permalink
feat: resolved TS issues related to making stripe optional
Browse files Browse the repository at this point in the history
  • Loading branch information
ousszizou committed Mar 18, 2024
1 parent 2b5c836 commit 527e24f
Show file tree
Hide file tree
Showing 10 changed files with 196 additions and 153 deletions.
16 changes: 7 additions & 9 deletions apps/www/actions/generate-user-stripe.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { redirect } from "next/navigation";
import { api } from "@/trpc/server";
import { currentUser } from "@clerk/nextjs";

import { stripe } from "@projectx/stripe";
import { withStripe } from "@projectx/stripe";

import { absoluteUrl } from "@/lib/utils";

Expand All @@ -18,10 +18,10 @@ const billingUrl = absoluteUrl("/pricing");

export async function generateUserStripe(
priceId: string,
): Promise<responseAction> {
let redirectUrl: string = "";
): Promise<responseAction | null> {
return withStripe<responseAction>(async (stripe) => {
let redirectUrl = "";

try {
const user = await currentUser();

if (!user || !user.emailAddresses) {
Expand Down Expand Up @@ -60,10 +60,8 @@ export async function generateUserStripe(

redirectUrl = stripeSession.url as string;
}
} catch (error) {
throw new Error("Failed to generate user stripe session");
}

// no revalidatePath because redirect
redirect(redirectUrl);
// no revalidatePath because redirect
redirect(redirectUrl);
});
}
9 changes: 5 additions & 4 deletions apps/www/app/(dashboard)/_components/workspace-switcher.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { toDecimal } from "dinero.js";
import { Check, ChevronDown, ChevronsUpDown, PlusCircle } from "lucide-react";

import { env } from "@projectx/stripe/env";
import type { ExtendedPlanInfo, PlansResponse } from "@projectx/stripe/plans";
import type { PurchaseOrg } from "@projectx/validators";
import { purchaseOrgSchema } from "@projectx/validators";

Expand Down Expand Up @@ -247,9 +248,9 @@ export function WorkspaceSwitcher({ isCollapsed }: WorkspaceSwitcherProps) {
function NewOrganizationDialog(props: { closeDialog: () => void }) {
const useStripe = env.USE_STRIPE === "true";

let plans: any = null;
let plans: any | null = null;
if (useStripe) {
plans = React.use(api.stripe.plans.query());
plans = api.stripe.plans.query();
}

const form = useZodForm({ schema: purchaseOrgSchema });
Expand All @@ -261,7 +262,7 @@ function NewOrganizationDialog(props: { closeDialog: () => void }) {
.mutate(data)
.catch(() => ({ success: false as const }));

if (response.success) window.location.href = response.url;
if (response?.success) window.location.href = response.url as string;
else
toaster.toast({
title: "Error",
Expand Down Expand Up @@ -324,7 +325,7 @@ function NewOrganizationDialog(props: { closeDialog: () => void }) {
</SelectTrigger>
</FormControl>
<SelectContent>
{plans.map((plan) => (
{plans?.map((plan: ExtendedPlanInfo) => (
<SelectItem key={plan.priceId} value={plan.priceId}>
<span className="font-medium">{plan.name}</span> -{" "}
<span className="text-muted-foreground">
Expand Down
14 changes: 5 additions & 9 deletions apps/www/app/api/webhooks/stripe/route.ts
Original file line number Diff line number Diff line change
@@ -1,28 +1,24 @@
import { NextResponse } from "next/server";
import type { NextRequest } from "next/server";

import { handleEvent, stripe } from "@projectx/stripe";
import { handleEvent, withStripe } from "@projectx/stripe";

import { env } from "@/env";

export async function POST(req: NextRequest) {
const payload = await req.text();
const signature = req.headers.get("Stripe-Signature")!;
const signature = req.headers.get("Stripe-Signature") as string;

try {
return withStripe(async (stripe) => {
const event = stripe.webhooks.constructEvent(
payload,
signature,
env.STRIPE_WEBHOOK_SECRET,
env.STRIPE_WEBHOOK_SECRET as string,
);

await handleEvent(event);

console.log("✅ Handled Stripe Event", event.type);
return NextResponse.json({ received: true }, { status: 200 });
} catch (error) {
const message = error instanceof Error ? error.message : "Unknown error";
console.log(`❌ Error when handling Stripe Event: ${message}`);
return NextResponse.json({ error: message }, { status: 400 });
}
});
}
2 changes: 1 addition & 1 deletion apps/www/types/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,6 @@ export type SubscriptionPlan = {
monthly: number;
};
stripeIds: {
monthly: string | null;
monthly: string | null | undefined;
};
};
196 changes: 98 additions & 98 deletions packages/api/src/router/stripe.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import { currentUser } from "@clerk/nextjs";
// import * as currencies from "@dinero.js/currencies";
import { USD } from "@dinero.js/currencies";
import { dinero } from "dinero.js";
import * as z from "zod";

import { eq, schema } from "@projectx/db";
import { PLANS, stripe } from "@projectx/stripe";
import { PLANS, withStripe, type PlansResponse } from "@projectx/stripe";
import { purchaseOrgSchema } from "@projectx/validators";

import { env } from "../env.mjs";
Expand All @@ -14,119 +14,119 @@ export const stripeRouter = createTRPCRouter({
createSession: protectedProcedure
.input(z.object({ planId: z.string() }))
.mutation(async (opts) => {
const { userId } = opts.ctx.auth;
return withStripe<{ success: boolean; url?: string }>(async (stripe) => {
const { userId } = opts.ctx.auth;

const customer = await opts.ctx.db
.select({
id: schema.customer.id,
plan: schema.customer.plan,
stripeId: schema.customer.stripeId,
})
.from(schema.customer)
.where(eq(schema.customer.clerkUserId, userId));

const returnUrl = `${env.NEXTJS_URL}/dashboard`;

if (customer?.[0] && customer[0].plan !== "FREE") {
/**
* User is subscribed, create a billing portal session
*/
const session = await stripe.billingPortal.sessions.create({
customer: customer[0].stripeId,
return_url: returnUrl,
});
return {
success: true as const,
url: session.url,
};
}

const customer = await opts.ctx.db
.select({
id: schema.customer.id,
plan: schema.customer.plan,
stripeId: schema.customer.stripeId,
})
.from(schema.customer)
.where(eq(schema.customer.clerkUserId, userId));

const returnUrl = `${env.NEXTJS_URL}/dashboard`;

if (customer?.[0] && customer[0].plan !== "FREE") {
/**
* User is subscribed, create a billing portal session
* User is not subscribed, create a checkout session
* Use existing email address if available
*/
const session = await stripe.billingPortal.sessions.create({
customer: customer[0].stripeId,
return_url: returnUrl,

const user = await currentUser();
const email = user?.emailAddresses.find(
(addr) => addr.id === user?.primaryEmailAddressId,
)?.emailAddress;

const session = await stripe.checkout.sessions.create({
mode: "subscription",
payment_method_types: ["card"],
customer_email: email,
client_reference_id: userId,
subscription_data: { metadata: { userId } },
cancel_url: returnUrl,
success_url: returnUrl,
line_items: [{ price: PLANS.PRO?.priceId, quantity: 1 }],
});

if (!session.url) return { success: false as const };
return {
success: true as const,
url: session.url,
};
}

/**
* User is not subscribed, create a checkout session
* Use existing email address if available
*/

const user = await currentUser();
const email = user?.emailAddresses.find(
(addr) => addr.id === user?.primaryEmailAddressId,
)?.emailAddress;

const session = await stripe.checkout.sessions.create({
mode: "subscription",
payment_method_types: ["card"],
customer_email: email,
client_reference_id: userId,
subscription_data: { metadata: { userId } },
cancel_url: returnUrl,
success_url: returnUrl,
line_items: [{ price: PLANS.PRO?.priceId, quantity: 1 }],
});

if (!session.url) return { success: false as const };
return {
success: true as const,
url: session.url,
};
}),

plans: publicProcedure.query(async () => {
const proPrice = await stripe.prices.retrieve(PLANS.PRO.priceId);
const stdPrice = await stripe.prices.retrieve(PLANS.STANDARD.priceId);

return [
{
...PLANS.STANDARD,
price: dinero({
amount: stdPrice.unit_amount!,
currency: {
code: "USD",
base: 10,
exponent: 2,
},
// currencies[stdPrice.currency as keyof typeof currencies] ??
// currencies.EUR,
}),
},
{
...PLANS.PRO,
price: dinero({
amount: proPrice.unit_amount!,
currency: {
code: "USD",
base: 10,
exponent: 2,
},
// currencies[proPrice.currency as keyof typeof currencies] ??
// currencies.EUR,
}),
},
];
withStripe<PlansResponse>(async (stripe) => {
const proPrice = await stripe.prices.retrieve(PLANS.PRO?.priceId || "");
const stdPrice = await stripe.prices.retrieve(
PLANS.STANDARD?.priceId || "",
);

return [
PLANS.STANDARD
? {
...PLANS.STANDARD,
price: dinero({
amount: stdPrice.unit_amount || 0,
currency: USD,
}),
}
: undefined,
PLANS.PRO
? {
...PLANS.PRO,
price: dinero({
amount: proPrice.unit_amount || 0,
currency: USD,
}),
}
: undefined,
].filter(Boolean) as PlansResponse;
});
}),

purchaseOrg: protectedProcedure
.input(purchaseOrgSchema)
.mutation(async (opts) => {
const { userId } = opts.ctx.auth;
const { orgName, planId } = opts.input;

const baseUrl = new URL(opts.ctx.req?.nextUrl ?? env.NEXTJS_URL).origin;

const session = await stripe.checkout.sessions.create({
mode: "subscription",
payment_method_types: ["card"],
client_reference_id: userId,
subscription_data: {
metadata: { userId, organizationName: orgName },
},
success_url: `${baseUrl}/onboarding`,
cancel_url: baseUrl,
line_items: [{ price: planId, quantity: 1 }],
});
return withStripe<{ success: boolean; url?: string }>(async (stripe) => {
const { userId } = opts.ctx.auth;
const { orgName, planId } = opts.input;

const baseUrl = new URL(opts.ctx.req?.nextUrl ?? env.NEXTJS_URL).origin;

const session = await stripe.checkout.sessions.create({
mode: "subscription",
payment_method_types: ["card"],
client_reference_id: userId,
subscription_data: {
metadata: { userId, organizationName: orgName },
},
success_url: `${baseUrl}/onboarding`,
cancel_url: baseUrl,
line_items: [{ price: planId, quantity: 1 }],
});

if (!session.url) return { success: false as const };
return {
success: true as const,
url: session.url,
};
if (!session.url) return { success: false as const };
return {
success: true as const,
url: session.url,
};
});
}),
});
23 changes: 12 additions & 11 deletions packages/stripe/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,20 +4,21 @@ import { env } from "./env.mjs";

export * from "./plans";
export * from "./webhooks";
export * from "./utils";

export type { Stripe };

let stripe: Stripe | undefined;

const useStripe = env.USE_STRIPE === "true";

if (useStripe && env.STRIPE_API_KEY) {
stripe = new Stripe(env.STRIPE_API_KEY, {
apiVersion: "2023-10-16",
typescript: true,
});
} else {
export const initializeStripe = (): Stripe | undefined => {
if (env.USE_STRIPE === "true" && env.STRIPE_API_KEY) {
return new Stripe(env.STRIPE_API_KEY, {
apiVersion: "2023-10-16",
typescript: true,
});
}
console.log("Stripe integration is disabled or not properly configured.");
}
return undefined;
};

const stripe = initializeStripe();

export { stripe };
Loading

0 comments on commit 527e24f

Please sign in to comment.