Skip to content

Instantly share code, notes, and snippets.

@kitze
Last active March 21, 2024 07:59
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save kitze/112fcb1bfcbc60dcfd9500ba3a6a6196 to your computer and use it in GitHub Desktop.
Save kitze/112fcb1bfcbc60dcfd9500ba3a6a6196 to your computer and use it in GitHub Desktop.
lemon squeezy helpers
import { NextApiRequest, NextApiResponse } from "next";
import { validateLemonSqueezyHook } from "@/pages/api/lemon/validateLemonSqueezyHook";
import getRawBody from "raw-body";
import { LemonEventType, ResBody } from "@/pages/api/lemon/types";
import { onOrderCreated } from "@/pages/api/lemon/hooks/onOrderCreated";
import { returnError, returnOkay } from "@/pages/api/lemon/utils";
export const config = {
api: {
bodyParser: false,
},
};
const handler = async (req: NextApiRequest, res: NextApiResponse) => {
console.log("🍋: hello");
console.log("req.method", req.method);
if (req.method !== "POST") {
console.log("🍋: method not allowed");
return res.status(405).json({
message: "Method not allowed",
});
}
console.log("req.method is allowed");
try {
const rawBody = await getRawBody(req);
const isValidHook = await validateLemonSqueezyHook({ req, rawBody });
console.log("🍋: isValidHook", isValidHook);
if (!isValidHook) {
return res.status(400).json({
message: "Invalid signature.",
});
}
//@ts-ignore
const event: ResBody["body"] = JSON.parse(rawBody);
const eventType = event.meta.event_name;
console.log("🍋: event type", eventType);
const handlers = {
[LemonEventType.OrderCreated]: onOrderCreated,
};
const foundHandler = handlers[eventType];
if (foundHandler) {
try {
await foundHandler({ event });
returnOkay(res);
} catch (err) {
console.log(`🍋: error in handling ${eventType} event`, err);
returnError(res);
}
} else {
console.log(`🍋: no handler found for ${eventType} event`);
}
console.log("eventType", eventType);
} catch (e: unknown) {
if (typeof e === "string") {
return res.status(400).json({
message: `Webhook error: ${e}`,
});
}
if (e instanceof Error) {
return res.status(400).json({
message: `Webhook error: ${e.message}`,
});
}
throw e;
}
};
export default handler;
import { LemonsqueezySubscriptionPause } from "./client/methods/updateSubscription/types";
import { NextApiRequest } from "next";
export enum LemonEventType {
SubCreated = "subscription_created",
SubUpdated = "subscription_updated",
SubPaymentSuccess = "subscription_payment_success",
OrderCreated = "order_created",
}
export type CustomLemonSqueezyCheckoutData = {
user_id: string;
};
export type LemonMeta = {
test_mode: boolean;
event_name: LemonEventType;
custom_data: CustomLemonSqueezyCheckoutData;
};
export type SubscriptionCreatedUpdatedCommon = {
type: string;
id: string;
attributes: {
store_id: number;
customer_id: number;
order_id: number;
order_item_id: number;
product_id: number;
variant_id: number;
product_name: string;
variant_name: string;
user_name: string;
user_email: string;
status: string;
status_formatted: string;
card_brand: string;
card_last_four: string;
pause: null | LemonsqueezySubscriptionPause;
cancelled: boolean;
trial_ends_at: null | Date;
billing_anchor: number;
urls: Record<string, string>;
renews_at: string;
ends_at: null | Date;
created_at: string;
updated_at: string;
test_mode: boolean;
};
relationships?: {
store: Record<string, unknown>;
customer: Record<string, unknown>;
order: Record<string, unknown>;
"order-item": Record<string, unknown>;
product: Record<string, unknown>;
variant: Record<string, unknown>;
"subscription-invoices": Record<string, string>;
};
links?: {
self: string;
};
};
type SubscriptionCreated = Omit<
SubscriptionCreatedUpdatedCommon,
"type" | "relationships" | "links"
> & {
type: string;
relationships: SubscriptionCreatedUpdatedCommon["relationships"];
links: SubscriptionCreatedUpdatedCommon["links"];
};
type SubscriptionUpdated = Omit<
SubscriptionCreatedUpdatedCommon,
"type" | "relationships" | "links"
> & {
type: "subscriptions";
};
export type SubscriptionPaymentSuccess = {
type: "subscription-invoices";
id: string;
attributes: {
store_id: number;
subscription_id: number;
billing_reason: string;
card_brand: string;
card_last_four: string;
currency: string;
currency_rate: string;
subtotal: number;
discount_total: number;
tax: number;
total: number;
subtotal_usd: number;
discount_total_usd: number;
tax_usd: number;
total_usd: number;
status: string;
status_formatted: string;
refunded: boolean;
refunded_at: string | null;
subtotal_formatted: string;
discount_total_formatted: string;
tax_formatted: string;
total_formatted: string;
urls: Record<string, unknown>;
created_at: string;
updated_at: string;
test_mode: boolean;
};
relationships: {
store: {
data: {
type: "stores";
id: string;
};
};
subscription: {
data: {
type: "subscriptions";
id: string;
};
};
};
links: {
self: string;
};
};
export type SubscriptionCreatedEvent = {
meta: LemonMeta;
data: SubscriptionCreated;
};
export type SubscriptionUpdatedEvent = {
meta: LemonMeta;
data: SubscriptionUpdated;
};
export type SubscriptionPaymentSuccessEvent = {
meta: LemonMeta;
data: SubscriptionPaymentSuccess;
};
export type LemonEvent =
| SubscriptionCreatedEvent
| SubscriptionUpdatedEvent
| SubscriptionPaymentSuccessEvent;
export interface ResBody extends NextApiRequest {
body: LemonEvent;
}
import { NextApiRequest } from "next";
import crypto from "crypto";
import { env } from "@/env.mjs";
export const validateLemonSqueezyHook = async ({
req,
rawBody,
}: {
req: NextApiRequest;
rawBody: any;
}): Promise<boolean> => {
try {
const hmac = crypto.createHmac("sha256", env.LEMONSQUEEZY_WEBHOOK_SECRET);
const digest = Buffer.from(hmac.update(rawBody).digest("hex"), "utf8");
const signature = Buffer.from(req.headers["x-signature"] as string, "utf8");
let validated = crypto.timingSafeEqual(digest, signature);
return validated;
} catch (err) {
console.log("err", err);
return false;
}
return false;
};
@kitze
Copy link
Author

kitze commented Dec 20, 2023

I open sourced all the lemon squeezy helpers here

@kitze
Copy link
Author

kitze commented Dec 20, 2023 via email

@fcristel
Copy link

You can find all the lemon squeezy helpers here https://github.com/kitze/lemon-squeezy-helpers

-- [image: avatar] Kitze Founder of Sizzy Benji - the ultimate welness & productivity app Zero To Shipped - Master Fullstack development
On December 19, 2023 at 4:25 PM, Cristian Furcila @.***) wrote: @fcristel commented on this gist. you're missing some files here I guess: @/pages/api/lemon/hooks/onOrderCreated @/pages/api/lemon/utils ./client/methods/updateSubscription/types — Reply to this email directly, view it on GitHub https://gist.github.com/kitze/112fcb1bfcbc60dcfd9500ba3a6a6196#gistcomment-4800246 or unsubscribe https://github.com/notifications/unsubscribe-auth/AAI3LEUQDCOME4HML5H2PJLYKGWYDBFKMF2HI4TJMJ2XIZLTSKBKK5TBNR2WLJDHNFZXJJDOMFWWLK3UNBZGKYLEL52HS4DFQKSXMYLMOVS2I5DSOVS2I3TBNVS3W5DIOJSWCZC7OBQXE5DJMNUXAYLOORPWCY3UNF3GS5DZVRZXKYTKMVRXIX3UPFYGLK2HNFZXIQ3PNVWWK3TUUZ2G64DJMNZZDAVEOR4XAZNEM5UXG5FFOZQWY5LFVEYTENZQGA3TENBUU52HE2LHM5SXFJTDOJSWC5DF. You are receiving this email because you authored the thread. Triage notifications on the go with GitHub Mobile for iOS https://apps.apple.com/app/apple-store/id1477376905?ct=notification-email&mt=8&pt=524675 or Android https://play.google.com/store/apps/details?id=com.github.android&referrer=utm_campaign%3Dnotification-email%26utm_medium%3Demail%26utm_source%3Dgithub.

Link is not working

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment