Last active
March 21, 2024 07:59
-
-
Save kitze/112fcb1bfcbc60dcfd9500ba3a6a6196 to your computer and use it in GitHub Desktop.
lemon squeezy helpers
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | |
}; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Link is not working