From 438d9debfcb78f08b8a6aa36285f0737614322a6 Mon Sep 17 00:00:00 2001
From: Emil Widlund <hello@emilwidlund.com>
Date: Sun, 19 Jan 2025 15:36:32 +0100
Subject: [PATCH] implement entitlements

---
 .../src/entitlement/entitlement.test.ts       | 176 +++---
 .../src/entitlement/entitlement.ts            | 136 ++---
 .../src/entitlements/Figma/Figma.test.ts      |  41 --
 .../src/entitlements/Figma/Figma.ts           |  15 -
 .../src/webhooks/webhooks.test.ts             | 540 ++++++++++--------
 .../adapter-utils/src/webhooks/webhooks.ts    |  14 +-
 6 files changed, 466 insertions(+), 456 deletions(-)
 delete mode 100644 packages/adapter-utils/src/entitlements/Figma/Figma.test.ts
 delete mode 100644 packages/adapter-utils/src/entitlements/Figma/Figma.ts

diff --git a/packages/adapter-utils/src/entitlement/entitlement.test.ts b/packages/adapter-utils/src/entitlement/entitlement.test.ts
index d8f0dc5..b698d10 100644
--- a/packages/adapter-utils/src/entitlement/entitlement.test.ts
+++ b/packages/adapter-utils/src/entitlement/entitlement.test.ts
@@ -1,104 +1,106 @@
 import { describe, it, expect, vi } from "vitest";
-import { Entitlement } from "./entitlement";
 import type {
-	Benefit,
-	WebhookBenefitGrantCreatedPayload,
-	WebhookBenefitGrantRevokedPayload,
+  Benefit,
+  WebhookBenefitGrantCreatedPayload,
+  WebhookBenefitGrantRevokedPayload,
 } from "@polar-sh/sdk/models/components";
+import { EntitlementStrategy } from "./entitlement";
 
-describe("Entitlement", () => {
-	it("should run grant on handler", () => {
-		const onGrant = vi.fn();
-		const onRevoke = vi.fn();
+describe("EntitlementStrategy", () => {
+  it("should run grant on handler", () => {
+    const onGrant = vi.fn();
+    const onRevoke = vi.fn();
 
-		const entitlement = Entitlement<{ test: string }>().grant(onGrant);
+    const entitlement = new EntitlementStrategy<{ test: string }>().grant(
+      onGrant,
+    );
 
-		expect(entitlement).toBeDefined();
+    expect(entitlement).toBeDefined();
 
-		const payload = {
-			type: "benefit_grant.created",
-			data: {
-				id: "123",
-				createdAt: new Date(),
-				modifiedAt: new Date(),
-				isGranted: true,
-				benefitId: "123",
-				customerId: "123",
-				subscriptionId: "123",
-				orderId: "123",
-				userId: "123",
-				isRevoked: false,
-				properties: { test: "test" },
-				customer: {
-					email: "test@test.com",
-					id: "123",
-					createdAt: new Date(),
-					modifiedAt: new Date(),
-					metadata: {},
-					emailVerified: true,
-					billingAddress: {
-						line1: "123",
-						line2: "123",
-						city: "123",
-						state: "123",
-						postalCode: "123",
-						country: "123",
-					},
-					name: "Test",
-					taxId: ["123"],
-					organizationId: "123",
-					avatarUrl: "123",
-				},
-				benefit: {
-					id: "123",
-					createdAt: new Date(),
-					modifiedAt: new Date(),
-					description: "Test",
-					selectable: true,
-					slug: "test",
-				} as unknown as Benefit,
-			},
-		} as WebhookBenefitGrantCreatedPayload;
+    const payload = {
+      type: "benefit_grant.created",
+      data: {
+        id: "123",
+        createdAt: new Date(),
+        modifiedAt: new Date(),
+        isGranted: true,
+        benefitId: "123",
+        customerId: "123",
+        subscriptionId: "123",
+        orderId: "123",
+        userId: "123",
+        isRevoked: false,
+        properties: { test: "test" },
+        customer: {
+          email: "test@test.com",
+          id: "123",
+          createdAt: new Date(),
+          modifiedAt: new Date(),
+          metadata: {},
+          emailVerified: true,
+          billingAddress: {
+            line1: "123",
+            line2: "123",
+            city: "123",
+            state: "123",
+            postalCode: "123",
+            country: "123",
+          },
+          name: "Test",
+          taxId: ["123"],
+          organizationId: "123",
+          avatarUrl: "123",
+        },
+        benefit: {
+          id: "123",
+          createdAt: new Date(),
+          modifiedAt: new Date(),
+          selectable: true,
+          description: "test",
+        } as unknown as Benefit,
+      },
+    } as WebhookBenefitGrantCreatedPayload;
 
-		entitlement.handler("test")(payload);
+    entitlement.handler("test")(payload);
 
-		expect(onGrant).toHaveBeenCalledWith({
-			payload,
-			customer: payload.data.customer,
-			properties: payload.data.properties,
-		});
+    expect(onGrant).toHaveBeenCalledWith({
+      payload,
+      customer: payload.data.customer,
+      properties: payload.data.properties,
+    });
 
-		expect(onRevoke).not.toHaveBeenCalled();
-	});
+    expect(onRevoke).not.toHaveBeenCalled();
+  });
 
-	it("should run revoke on handler", () => {
-		const onGrant = vi.fn();
-		const onRevoke = vi.fn();
+  it("should run revoke on handler", () => {
+    const onGrant = vi.fn();
+    const onRevoke = vi.fn();
 
-		const entitlement = Entitlement<{ test: string }>()
-			.grant(onGrant)
-			.revoke(onRevoke);
+    const entitlement = new EntitlementStrategy<{ test: string }>()
+      .grant(onGrant)
+      .revoke(onRevoke);
 
-		const payload = {
-			type: "benefit_grant.revoked",
-			data: {
-				id: "123",
-				createdAt: new Date(),
-				modifiedAt: new Date(),
-				isGranted: false,
-				benefitId: "123",
-				customerId: "123",
-			},
-		} as WebhookBenefitGrantRevokedPayload;
+    const payload = {
+      type: "benefit_grant.revoked",
+      data: {
+        id: "123",
+        createdAt: new Date(),
+        modifiedAt: new Date(),
+        isGranted: false,
+        benefitId: "123",
+        customerId: "123",
+        benefit: { description: "test" },
+      },
+    } as WebhookBenefitGrantRevokedPayload;
 
-		entitlement.handler("test")(payload);
+    entitlement.handler("test")(payload);
 
-		expect(onGrant).not.toHaveBeenCalled();
+    expect(onGrant).not.toHaveBeenCalled();
 
-		expect(onRevoke).toHaveBeenCalledWith({
-			payload,
-			customer: payload.data.customer,
-			properties: payload.data.properties,
-		});
-	});
+    expect(onRevoke).toHaveBeenCalledWith({
+      payload,
+      customer: payload.data.customer,
+      properties: payload.data.properties,
+    });
+  });
 });
diff --git a/packages/adapter-utils/src/entitlement/entitlement.ts b/packages/adapter-utils/src/entitlement/entitlement.ts
index b98939d..921d237 100644
--- a/packages/adapter-utils/src/entitlement/entitlement.ts
+++ b/packages/adapter-utils/src/entitlement/entitlement.ts
@@ -1,81 +1,89 @@
 import type {
-	Customer,
-	WebhookBenefitGrantCreatedPayload,
-	WebhookBenefitGrantRevokedPayload,
+  Customer,
+  WebhookBenefitGrantCreatedPayload,
+  WebhookBenefitGrantRevokedPayload,
 } from "@polar-sh/sdk/models/components";
 
 export type EntitlementProperties = Record<string, string>;
 
 export type EntitlementHandler = (
-	payload:
-		| WebhookBenefitGrantCreatedPayload
-		| WebhookBenefitGrantRevokedPayload,
+  payload:
+    | WebhookBenefitGrantCreatedPayload
+    | WebhookBenefitGrantRevokedPayload,
 ) => Promise<void>;
 
 export interface EntitlementContext<T extends EntitlementProperties> {
-	customer: Customer;
-	properties: T;
-	payload:
-		| WebhookBenefitGrantCreatedPayload
-		| WebhookBenefitGrantRevokedPayload;
+  customer: Customer;
+  properties: T;
+  payload:
+    | WebhookBenefitGrantCreatedPayload
+    | WebhookBenefitGrantRevokedPayload;
 }
 
 export class EntitlementStrategy<T extends EntitlementProperties> {
-	private grantCallbacks: ((
-		context: EntitlementContext<T>,
-	) => Promise<void>)[] = [];
-	private revokeCallbacks: ((
-		context: EntitlementContext<T>,
-	) => Promise<void>)[] = [];
+  private grantCallbacks: ((
+    context: EntitlementContext<T>,
+  ) => Promise<void>)[] = [];
 
-	public grant(callback: (context: EntitlementContext<T>) => Promise<void>) {
-		this.grantCallbacks.push(callback);
-		return this;
-	}
+  private revokeCallbacks: ((
+    context: EntitlementContext<T>,
+  ) => Promise<void>)[] = [];
 
-	public revoke(callback: (context: EntitlementContext<T>) => Promise<void>) {
-		this.revokeCallbacks.push(callback);
-		return this;
-	}
+  public grant(callback: (context: EntitlementContext<T>) => Promise<void>) {
+    this.grantCallbacks.push(callback);
+    return this;
+  }
 
-	public handler(slug: string): EntitlementHandler {
-		return async (
-			payload:
-				| WebhookBenefitGrantCreatedPayload
-				| WebhookBenefitGrantRevokedPayload,
-		) => {
-			if (payload.data.benefit.slug === slug) {
-				switch (payload.type) {
-					case "benefit_grant.created":
-						await Promise.all(
-							this.grantCallbacks.map((callback) =>
-								callback({
-									customer: payload.data.customer,
-									properties: payload.data.properties as T,
-									payload,
-								}),
-							),
-						);
-						break;
-					case "benefit_grant.revoked":
-						await Promise.all(
-							this.revokeCallbacks.map((callback) =>
-								callback({
-									customer: payload.data.customer,
-									properties: payload.data.properties as T,
-									payload,
-								}),
-							),
-						);
-						break;
-				}
-			}
-		};
-	}
+  public revoke(callback: (context: EntitlementContext<T>) => Promise<void>) {
+    this.revokeCallbacks.push(callback);
+    return this;
+  }
+
+  public handler(slug: string): EntitlementHandler {
+    return async (
+      payload:
+        | WebhookBenefitGrantCreatedPayload
+        | WebhookBenefitGrantRevokedPayload,
+    ) => {
+      if (payload.data.benefit.description === slug) {
+        switch (payload.type) {
+          case "benefit_grant.created":
+            await Promise.all(
+              this.grantCallbacks.map((callback) =>
+                callback({
+                  customer: payload.data.customer,
+                  properties: payload.data.properties as T,
+                  payload,
+                }),
+              ),
+            );
+            break;
+          case "benefit_grant.revoked":
+            await Promise.all(
+              this.revokeCallbacks.map((callback) =>
+                callback({
+                  customer: payload.data.customer,
+                  properties: payload.data.properties as T,
+                  payload,
+                }),
+              ),
+            );
+            break;
+        }
+      }
+    };
+  }
 }
 
-export const Entitlement = <
-	T extends EntitlementProperties = EntitlementProperties,
->() => {
-	return new EntitlementStrategy<T>();
-};
+export class Entitlements {
+  static handlers = [] as EntitlementHandler[];
+
+  static use<T extends EntitlementProperties = EntitlementProperties>(
+    slug: string,
+    strategy: EntitlementStrategy<T>,
+  ) {
+    this.handlers.push(strategy.handler(slug));
+
+    return this;
+  }
+}
diff --git a/packages/adapter-utils/src/entitlements/Figma/Figma.test.ts b/packages/adapter-utils/src/entitlements/Figma/Figma.test.ts
deleted file mode 100644
index db76f83..0000000
--- a/packages/adapter-utils/src/entitlements/Figma/Figma.test.ts
+++ /dev/null
@@ -1,41 +0,0 @@
-import { it, describe, expect } from "vitest";
-import { FigmaTeam, type FigmaTeamEntitlementProperties } from "./Figma";
-import type { EntitlementContext } from "../../entitlement/entitlement";
-import type {
-	Benefit,
-	WebhookBenefitGrantCreatedPayload,
-} from "@polar-sh/sdk/models/components";
-import type { Customer } from "@polar-sh/sdk/models/components";
-
-describe("FigmaTeam", () => {
-	it("should grant access to a team", async () => {
-		const payload = {
-			type: "benefit_grant.created",
-			data: {
-				id: "123",
-				createdAt: new Date(),
-				modifiedAt: new Date(),
-				isGranted: true,
-				benefitId: "123",
-				customerId: "123",
-				subscriptionId: "123",
-				orderId: "123",
-				userId: "123",
-				isRevoked: false,
-				customer: {
-					email: "test@test.com",
-				} as Customer,
-				properties: {
-					figmaTeamId: "123",
-				},
-				benefit: {
-					id: "123",
-				} as Benefit,
-			},
-		} as WebhookBenefitGrantCreatedPayload;
-
-		const entitlement = FigmaTeam(payload);
-
-		expect(entitlement).toBeDefined();
-	});
-});
diff --git a/packages/adapter-utils/src/entitlements/Figma/Figma.ts b/packages/adapter-utils/src/entitlements/Figma/Figma.ts
deleted file mode 100644
index 0b0fee5..0000000
--- a/packages/adapter-utils/src/entitlements/Figma/Figma.ts
+++ /dev/null
@@ -1,15 +0,0 @@
-import { Entitlement } from "../../entitlement/entitlement";
-
-export interface FigmaTeamEntitlementProperties {
-	[key: string]: string;
-	figmaTeamId: string;
-}
-
-export const FigmaTeam = Entitlement<FigmaTeamEntitlementProperties>()
-	.grant(async (context) => {
-		/** figma.team.addMember(context.properties.figmaTeamId, context.customer.email) */
-	})
-	.revoke(async (context) => {
-		/** figma.team.removeMember(context.properties.figmaTeamId, context.customer.email) */
-	})
-	.handler("figma-team");
diff --git a/packages/adapter-utils/src/webhooks/webhooks.test.ts b/packages/adapter-utils/src/webhooks/webhooks.test.ts
index 5d617dc..178a38c 100644
--- a/packages/adapter-utils/src/webhooks/webhooks.test.ts
+++ b/packages/adapter-utils/src/webhooks/webhooks.test.ts
@@ -1,253 +1,297 @@
+import {
+  EntitlementProperties,
+  Entitlements,
+  EntitlementStrategy,
+} from "../entitlement/entitlement";
 import { handleWebhookPayload } from "./webhooks";
 import { describe, expect, it, vi } from "vitest";
 
 describe("webhooks", () => {
-	it("should handle webhook payload", async () => {
-		const onPayload = vi.fn();
-
-		await handleWebhookPayload({ type: "checkout.created", data: {} } as any, {
-			webhookSecret: "test",
-			onPayload,
-		});
-
-		expect(onPayload).toHaveBeenCalledWith({
-			type: "checkout.created",
-			data: {},
-		});
-	});
-
-	it("should handle webhook payload with checkout created", async () => {
-		const onCheckoutCreated = vi.fn();
-
-		await handleWebhookPayload({ type: "checkout.created", data: {} } as any, {
-			webhookSecret: "test",
-			onCheckoutCreated,
-		});
-		expect(onCheckoutCreated).toHaveBeenCalledWith({
-			type: "checkout.created",
-			data: {},
-		});
-	});
-
-	it("should handle webhook payload with checkout updated", async () => {
-		const onCheckoutUpdated = vi.fn();
-
-		await handleWebhookPayload({ type: "checkout.updated", data: {} } as any, {
-			webhookSecret: "test",
-			onCheckoutUpdated,
-		});
-		expect(onCheckoutUpdated).toHaveBeenCalledWith({
-			type: "checkout.updated",
-			data: {},
-		});
-	});
-
-	it("should handle webhook payload with order created", async () => {
-		const onOrderCreated = vi.fn();
-
-		await handleWebhookPayload({ type: "order.created", data: {} } as any, {
-			webhookSecret: "test",
-			onOrderCreated,
-		});
-		expect(onOrderCreated).toHaveBeenCalledWith({
-			type: "order.created",
-			data: {},
-		});
-	});
-
-	it("should handle webhook payload with subscription created", async () => {
-		const onSubscriptionCreated = vi.fn();
-
-		await handleWebhookPayload(
-			{ type: "subscription.created", data: {} } as any,
-			{
-				webhookSecret: "test",
-				onSubscriptionCreated,
-			},
-		);
-		expect(onSubscriptionCreated).toHaveBeenCalledWith({
-			type: "subscription.created",
-			data: {},
-		});
-	});
-
-	it("should handle webhook payload with subscription updated", async () => {
-		const onSubscriptionUpdated = vi.fn();
-
-		await handleWebhookPayload(
-			{ type: "subscription.updated", data: {} } as any,
-			{
-				webhookSecret: "test",
-				onSubscriptionUpdated,
-			},
-		);
-		expect(onSubscriptionUpdated).toHaveBeenCalledWith({
-			type: "subscription.updated",
-			data: {},
-		});
-	});
-
-	it("should handle webhook payload with subscription active", async () => {
-		const onSubscriptionActive = vi.fn();
-
-		await handleWebhookPayload(
-			{ type: "subscription.active", data: {} } as any,
-			{
-				webhookSecret: "test",
-				onSubscriptionActive,
-			},
-		);
-		expect(onSubscriptionActive).toHaveBeenCalledWith({
-			type: "subscription.active",
-			data: {},
-		});
-	});
-
-	it("should handle webhook payload with subscription canceled", async () => {
-		const onSubscriptionCanceled = vi.fn();
-
-		await handleWebhookPayload(
-			{ type: "subscription.canceled", data: {} } as any,
-			{
-				webhookSecret: "test",
-				onSubscriptionCanceled,
-			},
-		);
-		expect(onSubscriptionCanceled).toHaveBeenCalledWith({
-			type: "subscription.canceled",
-			data: {},
-		});
-	});
-
-	it("should handle webhook payload with subscription revoked", async () => {
-		const onSubscriptionRevoked = vi.fn();
-
-		await handleWebhookPayload(
-			{ type: "subscription.revoked", data: {} } as any,
-			{
-				webhookSecret: "test",
-				onSubscriptionRevoked,
-			},
-		);
-		expect(onSubscriptionRevoked).toHaveBeenCalledWith({
-			type: "subscription.revoked",
-			data: {},
-		});
-	});
-
-	it("should handle webhook payload with product created", async () => {
-		const onProductCreated = vi.fn();
-
-		await handleWebhookPayload({ type: "product.created", data: {} } as any, {
-			webhookSecret: "test",
-			onProductCreated,
-		});
-		expect(onProductCreated).toHaveBeenCalledWith({
-			type: "product.created",
-			data: {},
-		});
-	});
-
-	it("should handle webhook payload with product updated", async () => {
-		const onProductUpdated = vi.fn();
-
-		await handleWebhookPayload({ type: "product.updated", data: {} } as any, {
-			webhookSecret: "test",
-			onProductUpdated,
-		});
-		expect(onProductUpdated).toHaveBeenCalledWith({
-			type: "product.updated",
-			data: {},
-		});
-	});
-
-	it("should handle webhook payload with organization updated", async () => {
-		const onOrganizationUpdated = vi.fn();
-
-		await handleWebhookPayload(
-			{ type: "organization.updated", data: {} } as any,
-			{
-				webhookSecret: "test",
-				onOrganizationUpdated,
-			},
-		);
-		expect(onOrganizationUpdated).toHaveBeenCalledWith({
-			type: "organization.updated",
-			data: {},
-		});
-	});
-
-	it("should handle webhook payload with benefit created", async () => {
-		const onBenefitCreated = vi.fn();
-
-		await handleWebhookPayload({ type: "benefit.created", data: {} } as any, {
-			webhookSecret: "test",
-			onBenefitCreated,
-		});
-		expect(onBenefitCreated).toHaveBeenCalledWith({
-			type: "benefit.created",
-			data: {},
-		});
-	});
-
-	it("should handle webhook payload with benefit updated", async () => {
-		const onBenefitUpdated = vi.fn();
-
-		await handleWebhookPayload({ type: "benefit.updated", data: {} } as any, {
-			webhookSecret: "test",
-			onBenefitUpdated,
-		});
-		expect(onBenefitUpdated).toHaveBeenCalledWith({
-			type: "benefit.updated",
-			data: {},
-		});
-	});
-
-	it("should handle webhook payload with benefit grant created", async () => {
-		const onBenefitGrantCreated = vi.fn();
-
-		await handleWebhookPayload(
-			{ type: "benefit_grant.created", data: {} } as any,
-			{
-				webhookSecret: "test",
-				onBenefitGrantCreated,
-			},
-		);
-		expect(onBenefitGrantCreated).toHaveBeenCalledWith({
-			type: "benefit_grant.created",
-			data: {},
-		});
-	});
-
-	it("should handle webhook payload with benefit grant updated", async () => {
-		const onBenefitGrantUpdated = vi.fn();
-
-		await handleWebhookPayload(
-			{ type: "benefit_grant.updated", data: {} } as any,
-			{
-				webhookSecret: "test",
-				onBenefitGrantUpdated,
-			},
-		);
-		expect(onBenefitGrantUpdated).toHaveBeenCalledWith({
-			type: "benefit_grant.updated",
-			data: {},
-		});
-	});
-
-	it("should handle webhook payload with benefit grant revoked", async () => {
-		const onBenefitGrantRevoked = vi.fn();
-
-		await handleWebhookPayload(
-			{ type: "benefit_grant.revoked", data: {} } as any,
-			{
-				webhookSecret: "test",
-				onBenefitGrantRevoked,
-			},
-		);
-		expect(onBenefitGrantRevoked).toHaveBeenCalledWith({
-			type: "benefit_grant.revoked",
-			data: {},
-		});
-	});
+  it("should handle webhook payload", async () => {
+    const onPayload = vi.fn();
+
+    await handleWebhookPayload({ type: "checkout.created", data: {} } as any, {
+      webhookSecret: "test",
+      onPayload,
+    });
+
+    expect(onPayload).toHaveBeenCalledWith({
+      type: "checkout.created",
+      data: {},
+    });
+  });
+
+  it("should handle webhook payload with checkout created", async () => {
+    const onCheckoutCreated = vi.fn();
+
+    await handleWebhookPayload({ type: "checkout.created", data: {} } as any, {
+      webhookSecret: "test",
+      onCheckoutCreated,
+    });
+    expect(onCheckoutCreated).toHaveBeenCalledWith({
+      type: "checkout.created",
+      data: {},
+    });
+  });
+
+  it("should handle webhook payload with checkout updated", async () => {
+    const onCheckoutUpdated = vi.fn();
+
+    await handleWebhookPayload({ type: "checkout.updated", data: {} } as any, {
+      webhookSecret: "test",
+      onCheckoutUpdated,
+    });
+    expect(onCheckoutUpdated).toHaveBeenCalledWith({
+      type: "checkout.updated",
+      data: {},
+    });
+  });
+
+  it("should handle webhook payload with order created", async () => {
+    const onOrderCreated = vi.fn();
+
+    await handleWebhookPayload({ type: "order.created", data: {} } as any, {
+      webhookSecret: "test",
+      onOrderCreated,
+    });
+    expect(onOrderCreated).toHaveBeenCalledWith({
+      type: "order.created",
+      data: {},
+    });
+  });
+
+  it("should handle webhook payload with subscription created", async () => {
+    const onSubscriptionCreated = vi.fn();
+
+    await handleWebhookPayload(
+      { type: "subscription.created", data: {} } as any,
+      {
+        webhookSecret: "test",
+        onSubscriptionCreated,
+      },
+    );
+    expect(onSubscriptionCreated).toHaveBeenCalledWith({
+      type: "subscription.created",
+      data: {},
+    });
+  });
+
+  it("should handle webhook payload with subscription updated", async () => {
+    const onSubscriptionUpdated = vi.fn();
+
+    await handleWebhookPayload(
+      { type: "subscription.updated", data: {} } as any,
+      {
+        webhookSecret: "test",
+        onSubscriptionUpdated,
+      },
+    );
+    expect(onSubscriptionUpdated).toHaveBeenCalledWith({
+      type: "subscription.updated",
+      data: {},
+    });
+  });
+
+  it("should handle webhook payload with subscription active", async () => {
+    const onSubscriptionActive = vi.fn();
+
+    await handleWebhookPayload(
+      { type: "subscription.active", data: {} } as any,
+      {
+        webhookSecret: "test",
+        onSubscriptionActive,
+      },
+    );
+    expect(onSubscriptionActive).toHaveBeenCalledWith({
+      type: "subscription.active",
+      data: {},
+    });
+  });
+
+  it("should handle webhook payload with subscription canceled", async () => {
+    const onSubscriptionCanceled = vi.fn();
+
+    await handleWebhookPayload(
+      { type: "subscription.canceled", data: {} } as any,
+      {
+        webhookSecret: "test",
+        onSubscriptionCanceled,
+      },
+    );
+    expect(onSubscriptionCanceled).toHaveBeenCalledWith({
+      type: "subscription.canceled",
+      data: {},
+    });
+  });
+
+  it("should handle webhook payload with subscription revoked", async () => {
+    const onSubscriptionRevoked = vi.fn();
+
+    await handleWebhookPayload(
+      { type: "subscription.revoked", data: {} } as any,
+      {
+        webhookSecret: "test",
+        onSubscriptionRevoked,
+      },
+    );
+    expect(onSubscriptionRevoked).toHaveBeenCalledWith({
+      type: "subscription.revoked",
+      data: {},
+    });
+  });
+
+  it("should handle webhook payload with product created", async () => {
+    const onProductCreated = vi.fn();
+
+    await handleWebhookPayload({ type: "product.created", data: {} } as any, {
+      webhookSecret: "test",
+      onProductCreated,
+    });
+    expect(onProductCreated).toHaveBeenCalledWith({
+      type: "product.created",
+      data: {},
+    });
+  });
+
+  it("should handle webhook payload with product updated", async () => {
+    const onProductUpdated = vi.fn();
+
+    await handleWebhookPayload({ type: "product.updated", data: {} } as any, {
+      webhookSecret: "test",
+      onProductUpdated,
+    });
+    expect(onProductUpdated).toHaveBeenCalledWith({
+      type: "product.updated",
+      data: {},
+    });
+  });
+
+  it("should handle webhook payload with organization updated", async () => {
+    const onOrganizationUpdated = vi.fn();
+
+    await handleWebhookPayload(
+      { type: "organization.updated", data: {} } as any,
+      {
+        webhookSecret: "test",
+        onOrganizationUpdated,
+      },
+    );
+    expect(onOrganizationUpdated).toHaveBeenCalledWith({
+      type: "organization.updated",
+      data: {},
+    });
+  });
+
+  it("should handle webhook payload with benefit created", async () => {
+    const onBenefitCreated = vi.fn();
+
+    await handleWebhookPayload({ type: "benefit.created", data: {} } as any, {
+      webhookSecret: "test",
+      onBenefitCreated,
+    });
+    expect(onBenefitCreated).toHaveBeenCalledWith({
+      type: "benefit.created",
+      data: {},
+    });
+  });
+
+  it("should handle webhook payload with benefit updated", async () => {
+    const onBenefitUpdated = vi.fn();
+
+    await handleWebhookPayload({ type: "benefit.updated", data: {} } as any, {
+      webhookSecret: "test",
+      onBenefitUpdated,
+    });
+    expect(onBenefitUpdated).toHaveBeenCalledWith({
+      type: "benefit.updated",
+      data: {},
+    });
+  });
+
+  it("should handle webhook payload with benefit grant created", async () => {
+    const onBenefitGrantCreated = vi.fn();
+
+    await handleWebhookPayload(
+      { type: "benefit_grant.created", data: {} } as any,
+      {
+        webhookSecret: "test",
+        onBenefitGrantCreated,
+      },
+    );
+    expect(onBenefitGrantCreated).toHaveBeenCalledWith({
+      type: "benefit_grant.created",
+      data: {},
+    });
+  });
+
+  it("should handle webhook payload with benefit grant updated", async () => {
+    const onBenefitGrantUpdated = vi.fn();
+
+    await handleWebhookPayload(
+      { type: "benefit_grant.updated", data: {} } as any,
+      {
+        webhookSecret: "test",
+        onBenefitGrantUpdated,
+      },
+    );
+    expect(onBenefitGrantUpdated).toHaveBeenCalledWith({
+      type: "benefit_grant.updated",
+      data: {},
+    });
+  });
+
+  it("should handle webhook payload with benefit grant revoked", async () => {
+    const onBenefitGrantRevoked = vi.fn();
+
+    await handleWebhookPayload(
+      { type: "benefit_grant.revoked", data: {} } as any,
+      {
+        webhookSecret: "test",
+        onBenefitGrantRevoked,
+      },
+    );
+    expect(onBenefitGrantRevoked).toHaveBeenCalledWith({
+      type: "benefit_grant.revoked",
+      data: {},
+    });
+  });
+
+  it("should run entitlement grant & revoke methods when applicable", async () => {
+    const onGrant = vi.fn();
+    const onRevoke = vi.fn();
+
+    const entitlementStrategy = new EntitlementStrategy()
+      .grant(onGrant)
+      .revoke(onRevoke);
+
+    await handleWebhookPayload(
+      {
+        type: "benefit_grant.created",
+        data: {
+          customer: {},
+          benefit: { description: "benefit-1", properties: {} },
+          properties: {},
+        },
+      } as any,
+      {
+        webhookSecret: "test",
+        entitlements: Entitlements.use("benefit-1", entitlementStrategy),
+      },
+    );
+
+    expect(onGrant).toHaveBeenCalledWith({
+      customer: {},
+      properties: {},
+      payload: {
+        type: "benefit_grant.created",
+        data: {
+          customer: {},
+          benefit: { description: "benefit-1", properties: {} },
+          properties: {},
+        },
+      },
+    });
+
+    expect(onRevoke).not.toHaveBeenCalled();
+  });
 });
diff --git a/packages/adapter-utils/src/webhooks/webhooks.ts b/packages/adapter-utils/src/webhooks/webhooks.ts
index 4d5432c..9d9fb05 100644
--- a/packages/adapter-utils/src/webhooks/webhooks.ts
+++ b/packages/adapter-utils/src/webhooks/webhooks.ts
@@ -17,9 +17,11 @@ import type {
   WebhookBenefitGrantUpdatedPayload,
   WebhookBenefitGrantRevokedPayload,
 } from "@polar-sh/sdk/models/components";
+import { Entitlements } from "../entitlement/entitlement";
 
 export interface WebhooksConfig {
   webhookSecret: string;
+  entitlements?: typeof Entitlements;
   onPayload?: (payload: ReturnType<typeof validateEvent>) => Promise<void>;
   onCheckoutCreated?: (payload: WebhookCheckoutCreatedPayload) => Promise<void>;
   onCheckoutUpdated?: (payload: WebhookCheckoutUpdatedPayload) => Promise<void>;
@@ -59,7 +61,7 @@ export interface WebhooksConfig {
 
 export const handleWebhookPayload = async (
   payload: ReturnType<typeof validateEvent>,
-  { webhookSecret, onPayload, ...eventHandlers }: WebhooksConfig,
+  { webhookSecret, entitlements, onPayload, ...eventHandlers }: WebhooksConfig,
 ) => {
   const promises: Promise<void>[] = [];
 
@@ -149,5 +151,15 @@ export const handleWebhookPayload = async (
       }
   }
 
+  switch (payload.type) {
+    case "benefit_grant.created":
+    case "benefit_grant.revoked":
+      if (entitlements) {
+        for (const handler of entitlements.handlers) {
+          promises.push(handler(payload));
+        }
+      }
+  }
+
   return Promise.all(promises);
 };