-
-
Save kitze/112fcb1bfcbc60dcfd9500ba3a6a6196 to your computer and use it in GitHub Desktop.
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; | |
}; |
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
I open sourced all the lemon squeezy helpers here