Skip to content

Commit

Permalink
Merge pull request #35 from kevinreber/fix-stripe-webhook
Browse files Browse the repository at this point in the history
Debugging Stripe Webhook
  • Loading branch information
kevinreber authored Nov 24, 2024
2 parents 3c24a8b + 71bdfcd commit dbff6fe
Show file tree
Hide file tree
Showing 6 changed files with 179 additions and 89 deletions.
43 changes: 27 additions & 16 deletions app/routes/checkout.tsx
Original file line number Diff line number Diff line change
@@ -1,31 +1,42 @@
import {
redirect,
type LoaderFunctionArgs,
MetaFunction,
type MetaFunction,
} from "@remix-run/node";
import { authenticator } from "~/services/auth.server";
import { requireUserLogin } from "~/services/auth.server";
import { stripeCheckout } from "~/services/stripe.server";
import { loader as UserLoaderData } from "../root";
import { Logger } from "~/utils/logger.server";

export const meta: MetaFunction<
typeof loader,
{ root: typeof UserLoaderData }
> = ({ matches }) => {
const userMatch = matches.find((match) => match.id === "root");
const username =
userMatch?.data?.userData?.username ||
userMatch?.data?.userData?.name ||
"Checkout";

return [{ title: `Checkout | ${username}` }];
};

export const loader = async ({ request }: LoaderFunctionArgs) => {
const user = (await authenticator.isAuthenticated(request, {
failureRedirect: "/",
})) as { id: string };
const user = await requireUserLogin(request);

Logger.info({
message: "[checkout.tsx]: Starting checkout process",
metadata: { userId: user.id },
});

const url = await stripeCheckout({
userId: user.id,
});

return redirect(url);
};

export const meta: MetaFunction<
typeof loader,
{ root: typeof UserLoaderData }
> = ({ data, params, matches }) => {
// Incase our Profile loader ever fails, we can get logged in user data from root
const userMatch = matches.find((match) => match.id === "root");
const username = userMatch?.data.data?.username || userMatch?.data.data?.name;
Logger.info({
message: "[checkout.tsx]: Redirecting to Stripe checkout",
metadata: { checkoutUrl: url },
});

return [{ title: `Checkout | ${username}` }];
return redirect(url);
};
83 changes: 74 additions & 9 deletions app/routes/webhook.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import type { ActionFunction, MetaFunction } from "@remix-run/node";
import { json } from "@remix-run/node";
import { stripe } from "~/services/stripe.server";
import { handleStripeEvent } from "~/services/webhook.server";
import { Logger } from "~/utils/logger.server";

export const meta: MetaFunction = () => {
return [{ title: "Stripe Webhook" }];
Expand All @@ -10,22 +11,86 @@ export const meta: MetaFunction = () => {
// [credit @kiliman to get this webhook working](https://github.com/remix-run/remix/discussions/1978)
// To have this webhook working locally, in another server we must run `stripe listen --forward-to localhost:3000/webhook` (yarn run stripe:listen)
export const action: ActionFunction = async ({ request }) => {
console.log("WEBHOOK REQUEST: ", request);
console.log("Webhook endpoint hit!", new Date().toISOString());

Logger.info({
message: "[webhook.ts]: Received webhook request",
metadata: {
method: request.method,
url: request.url,
headers: Object.fromEntries(request.headers.entries()),
timestamp: new Date().toISOString(),
},
});

// Only allow POST requests
if (request.method !== "POST") {
Logger.warn({
message: "[webhook.ts]: Invalid method for webhook",
metadata: { method: request.method },
});
return json({ error: "Method not allowed" }, 405);
}

const payload = await request.text();
const sig = request.headers.get("stripe-signature")!;
console.log("PAYLOAD: ", payload);
console.log("SIG: ", sig);

Logger.info({
message: "[webhook.ts]: Payload",
metadata: { payload },
});

const sig = request.headers.get("stripe-signature");

if (!sig) {
Logger.warn({
message: "[webhook.ts]: Missing Stripe signature",
});
return json({ error: "No signature" }, 400);
}

try {
const { type, data, id } = stripe.webhooks.constructEvent(
Logger.info({
message: "[webhook.ts]: Constructing Stripe event",
metadata: {
signaturePresent: !!sig,
payloadLength: payload.length,
webhookSecretPresent: !!process.env.STRIPE_WEB_HOOK_SECRET,
},
});

const event = stripe.webhooks.constructEvent(
payload,
sig,
process.env.STRIPE_WEB_HOOK_SECRET!
);

const userData = await handleStripeEvent(type, data, id);
Logger.info({
message: "[webhook.ts]: Successfully constructed Stripe event ",
metadata: {
eventType: event.type,
eventId: event.id,
},
});

return { data: userData };
} catch (error: any) {
throw json({ errors: [{ message: error.message }] }, 400);
const result = await handleStripeEvent(event.type, event.data, event.id);

return json({ success: true, data: result });
} catch (error: unknown) {
Logger.error({
message: "[webhook.ts]: Webhook error",
error: error instanceof Error ? error : new Error(String(error)),
metadata: {
payload: payload.substring(0, 100) + "...", // Log first 100 chars of payload
},
});

return json(
{
errors: [
{ message: error instanceof Error ? error.message : "Unknown error" },
],
},
400
);
}
};
17 changes: 11 additions & 6 deletions app/services/stripe.server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,29 +3,34 @@ import { json } from "@remix-run/node";
import { Logger } from "~/utils/logger.server";

export const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!, {
apiVersion: "2022-11-15",
apiVersion: "2024-11-20.acacia",
});

export const stripeCheckout = async ({ userId }: { userId: string }) => {
try {
Logger.info({
message: "Creating Stripe checkout session",
message: "[stripe.server]: Creating Stripe checkout session",
metadata: { userId },
});

const session = await stripe.checkout.sessions.create({
success_url: `${process.env.ORIGIN}/create`!,
cancel_url: `${process.env.ORIGIN}/create`!,
success_url: `${process.env.ORIGIN}/create`,
cancel_url: `${process.env.ORIGIN}/create`,
line_items: [{ price: process.env.STRIPE_CREDITS_PRICE_ID, quantity: 1 }],
mode: "payment",
metadata: {
userId,
},
payment_method_types: ["card", "us_bank_account"],
payment_intent_data: {
metadata: {
userId,
},
},
});

Logger.info({
message: "Stripe checkout session created successfully",
message: "[stripe.server]: Stripe checkout session created successfully",
metadata: {
userId,
sessionId: session.id,
Expand All @@ -35,7 +40,7 @@ export const stripeCheckout = async ({ userId }: { userId: string }) => {
return session.url!;
} catch (error: unknown) {
Logger.error({
message: "Failed to create Stripe checkout session",
message: "[stripe.server]: Failed to create Stripe checkout session",
error: error instanceof Error ? error : new Error(String(error)),
metadata: { userId },
});
Expand Down
113 changes: 61 additions & 52 deletions app/services/webhook.server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,71 +11,80 @@ export const handleStripeEvent = async (
) => {
try {
Logger.info({
message: "Processing Stripe webhook event",
metadata: { type, id }
message: "[webhook.server]: Processing Stripe webhook event",
metadata: { type, id, data: JSON.stringify(data) },
});

const isTestEvent = id === "evt_00000000000000";
if (isTestEvent) {
Logger.info({ message: "Skipping test event" });
return;
}

switch (type) {
case CHECKOUT_SESSION_COMPLETED: {
const checkoutSessionCompleted = data.object as {
id: string;
amount: number;
metadata: {
userId: string;
};
};

const creditsToAdd = 100;
Logger.info({
message: "Processing completed checkout session",
metadata: {
sessionId: checkoutSessionCompleted.id,
userId: checkoutSessionCompleted.metadata.userId,
creditsToAdd
}
});

const userData = await prisma.user.update({
where: {
id: checkoutSessionCompleted.metadata.userId,
},
data: {
credits: {
increment: creditsToAdd,
},
},
});

Logger.info({
message: "Successfully updated user credits",
metadata: {
userId: userData.id,
newCreditBalance: userData.credits
}
});

return userData;
const session = data.object as Stripe.Checkout.Session;
return handleCheckoutSession(session);
}

default:
Logger.warn({
message: "Unhandled webhook event type",
metadata: { type }
message: "[webhook.server]: Unhandled webhook event type",
metadata: { type },
});
return null;
}

return;
} catch (error) {
Logger.error({
message: "Error processing Stripe webhook",
message: "[webhook.server]: Error processing Stripe webhook",
error: error instanceof Error ? error : new Error(String(error)),
metadata: { type, id }
metadata: { type, id },
});
throw error;
}
};

const handleCheckoutSession = async (session: Stripe.Checkout.Session) => {
Logger.info({
message: "[webhook.server]: Processing checkout session",
metadata: {
sessionId: session.id,
metadata: session.metadata,
customerId: session.customer,
paymentStatus: session.payment_status,
},
});

if (!session.metadata?.userId) {
throw new Error("No userId found in session metadata");
}

return await updateUserCredits(session.metadata.userId);
};

const updateUserCredits = async (userId: string) => {
const creditsToAdd = 100;

Logger.info({
message: "[webhook.server]: Updating user credits",
metadata: {
userId,
creditsToAdd,
},
});

const userData = await prisma.user.update({
where: {
id: userId,
},
data: {
credits: {
increment: creditsToAdd,
},
},
});

Logger.info({
message: "[webhook.server]: Successfully updated user credits",
metadata: {
userId: userData.id,
newCreditBalance: userData.credits,
},
});

return userData;
};
8 changes: 4 additions & 4 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
"lint": "eslint --ignore-path .gitignore --cache --cache-location ./node_modules/.cache/eslint .",
"start": "remix-serve ./build/server/index.js",
"typecheck": "tsc",
"stripe:listen": "stripe listen --forward-to localhost:3000/webhook",
"stripe:listen": "stripe listen --forward-to localhost:5173/webhook",
"postinstall": "prisma generate"
},
"dependencies": {
Expand Down Expand Up @@ -54,7 +54,7 @@
"remix-auth-google": "^2.0.0",
"remix-utils": "^7.6.0",
"sonner": "^1.5.0",
"stripe": "^12.3.0",
"stripe": "^17.4.0",
"tailwind-merge": "^2.5.2",
"tailwindcss-animate": "^1.0.7",
"zod": "^3.23.8",
Expand Down

0 comments on commit dbff6fe

Please sign in to comment.