paddle-checkout-accelerator 2.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +7 -0
- package/README.md +388 -0
- package/dist/package/index.d.ts +28 -0
- package/dist/package/index.js +28 -0
- package/dist/src/components/paddle/BillingHistory.d.ts +1 -0
- package/dist/src/components/paddle/BillingHistory.js +17 -0
- package/dist/src/components/paddle/CustomerPortal.d.ts +1 -0
- package/dist/src/components/paddle/CustomerPortal.js +5 -0
- package/dist/src/components/paddle/CustomerPortalButton.d.ts +1 -0
- package/dist/src/components/paddle/CustomerPortalButton.js +31 -0
- package/dist/src/components/paddle/InlineCheckout.d.ts +5 -0
- package/dist/src/components/paddle/InlineCheckout.js +13 -0
- package/dist/src/components/paddle/PricingTable.d.ts +1 -0
- package/dist/src/components/paddle/PricingTable.js +27 -0
- package/dist/src/components/paddle/SubscriptionCard.d.ts +6 -0
- package/dist/src/components/paddle/SubscriptionCard.js +5 -0
- package/dist/src/components/paddle/TrialBanner.d.ts +5 -0
- package/dist/src/components/paddle/TrialBanner.js +5 -0
- package/dist/src/components/paddle/UpgradeModal.d.ts +11 -0
- package/dist/src/components/paddle/UpgradeModal.js +35 -0
- package/dist/src/components/paddle/UsageMeter.d.ts +6 -0
- package/dist/src/components/paddle/UsageMeter.js +8 -0
- package/dist/src/components/paddle/gates/SubscriptionGate.d.ts +7 -0
- package/dist/src/components/paddle/gates/SubscriptionGate.js +8 -0
- package/dist/src/lib/billing/adapters/index.d.ts +3 -0
- package/dist/src/lib/billing/adapters/index.js +3 -0
- package/dist/src/lib/billing/adapters/memory.d.ts +2 -0
- package/dist/src/lib/billing/adapters/memory.js +41 -0
- package/dist/src/lib/billing/adapters/prisma/index.d.ts +28 -0
- package/dist/src/lib/billing/adapters/prisma/index.js +80 -0
- package/dist/src/lib/billing/adapters/types.d.ts +13 -0
- package/dist/src/lib/billing/adapters/types.js +1 -0
- package/dist/src/lib/billing/configure.d.ts +6 -0
- package/dist/src/lib/billing/configure.js +13 -0
- package/dist/src/lib/billing/customer-repair.d.ts +4 -0
- package/dist/src/lib/billing/customer-repair.js +19 -0
- package/dist/src/lib/billing/demo-seed.d.ts +1 -0
- package/dist/src/lib/billing/demo-seed.js +13 -0
- package/dist/src/lib/billing/entitlements.d.ts +3 -0
- package/dist/src/lib/billing/entitlements.js +16 -0
- package/dist/src/lib/billing/events.d.ts +12 -0
- package/dist/src/lib/billing/events.js +20 -0
- package/dist/src/lib/billing/plans.d.ts +9 -0
- package/dist/src/lib/billing/plans.js +41 -0
- package/dist/src/lib/billing/protection.d.ts +6 -0
- package/dist/src/lib/billing/protection.js +16 -0
- package/dist/src/lib/billing/refresh.d.ts +1 -0
- package/dist/src/lib/billing/refresh.js +32 -0
- package/dist/src/lib/billing/subscriptions.d.ts +16 -0
- package/dist/src/lib/billing/subscriptions.js +36 -0
- package/dist/src/lib/billing/teams.d.ts +42 -0
- package/dist/src/lib/billing/teams.js +104 -0
- package/dist/src/lib/billing/usage.d.ts +17 -0
- package/dist/src/lib/billing/usage.js +40 -0
- package/dist/src/lib/billing/webhook-sync.d.ts +26 -0
- package/dist/src/lib/billing/webhook-sync.js +39 -0
- package/dist/src/lib/paddle/api.d.ts +1 -0
- package/dist/src/lib/paddle/api.js +21 -0
- package/dist/src/lib/paddle/client.d.ts +1 -0
- package/dist/src/lib/paddle/client.js +13 -0
- package/dist/src/lib/paddle/customers.d.ts +15 -0
- package/dist/src/lib/paddle/customers.js +14 -0
- package/dist/src/lib/paddle/events.d.ts +10 -0
- package/dist/src/lib/paddle/events.js +11 -0
- package/dist/src/lib/paddle/hooks.d.ts +4 -0
- package/dist/src/lib/paddle/hooks.js +11 -0
- package/dist/src/lib/paddle/portal.d.ts +8 -0
- package/dist/src/lib/paddle/portal.js +28 -0
- package/dist/src/lib/paddle/subscriptions.d.ts +20 -0
- package/dist/src/lib/paddle/subscriptions.js +10 -0
- package/dist/src/lib/paddle/types.d.ts +8 -0
- package/dist/src/lib/paddle/types.js +1 -0
- package/dist/src/lib/paddle/webhook.d.ts +1 -0
- package/dist/src/lib/paddle/webhook.js +8 -0
- package/dist/src/lib/utils.d.ts +2 -0
- package/dist/src/lib/utils.js +5 -0
- package/docs/agency-use-case.md +19 -0
- package/docs/quickstart.md +24 -0
- package/docs/recipes/prisma/adapter.example.txt +70 -0
- package/docs/recipes/prisma/client.example.txt +4 -0
- package/docs/recipes/prisma/schema.prisma +30 -0
- package/package.json +59 -0
- package/recipes/README.md +20 -0
- package/recipes/nextjs/app/billing.ts +14 -0
- package/recipes/nextjs/custom-adapter.ts +26 -0
- package/recipes/nextjs/protected-api-route.ts +27 -0
- package/recipes/nextjs/server-page-gate.tsx +18 -0
- package/recipes/nextjs/webhook-route.ts +23 -0
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { memoryBillingAdapter, } from "./adapters";
|
|
2
|
+
import { plans } from "./plans";
|
|
3
|
+
import { getSubscription } from "./subscriptions";
|
|
4
|
+
let adapter = memoryBillingAdapter;
|
|
5
|
+
export function setUsageAdapter(nextAdapter) {
|
|
6
|
+
adapter = nextAdapter;
|
|
7
|
+
}
|
|
8
|
+
function currentPeriod() {
|
|
9
|
+
const now = new Date();
|
|
10
|
+
return `${now.getUTCFullYear()}-${String(now.getUTCMonth() + 1).padStart(2, "0")}`;
|
|
11
|
+
}
|
|
12
|
+
export async function getUsage(userId, key) {
|
|
13
|
+
return adapter.getUsage(userId, key, currentPeriod());
|
|
14
|
+
}
|
|
15
|
+
export async function getUsageLimit(userId) {
|
|
16
|
+
const subscription = await getSubscription(userId);
|
|
17
|
+
return plans[subscription.plan].monthlyLimit;
|
|
18
|
+
}
|
|
19
|
+
export async function canUse(userId, key) {
|
|
20
|
+
const [used, limit] = await Promise.all([
|
|
21
|
+
getUsage(userId, key),
|
|
22
|
+
getUsageLimit(userId),
|
|
23
|
+
]);
|
|
24
|
+
return {
|
|
25
|
+
allowed: used < limit,
|
|
26
|
+
used,
|
|
27
|
+
limit,
|
|
28
|
+
remaining: Math.max(0, limit - used),
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
export async function incrementUsage(userId, key, amount = 1) {
|
|
32
|
+
return adapter.incrementUsage(userId, key, currentPeriod(), amount);
|
|
33
|
+
}
|
|
34
|
+
export async function requireUsage(userId, key) {
|
|
35
|
+
const usage = await canUse(userId, key);
|
|
36
|
+
if (!usage.allowed) {
|
|
37
|
+
throw new Error("Usage limit reached");
|
|
38
|
+
}
|
|
39
|
+
return usage;
|
|
40
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { PlanId } from "./plans";
|
|
2
|
+
interface PaddleEvent {
|
|
3
|
+
event_type?: string;
|
|
4
|
+
data?: {
|
|
5
|
+
id?: string;
|
|
6
|
+
status?: string;
|
|
7
|
+
customer_id?: string;
|
|
8
|
+
custom_data?: {
|
|
9
|
+
userId?: string;
|
|
10
|
+
plan?: PlanId;
|
|
11
|
+
};
|
|
12
|
+
current_billing_period?: {
|
|
13
|
+
ends_at?: string;
|
|
14
|
+
};
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
export declare function syncPaddleEvent(event: PaddleEvent): Promise<{
|
|
18
|
+
synced: boolean;
|
|
19
|
+
reason: string;
|
|
20
|
+
type?: undefined;
|
|
21
|
+
} | {
|
|
22
|
+
synced: boolean;
|
|
23
|
+
type: string;
|
|
24
|
+
reason?: undefined;
|
|
25
|
+
}>;
|
|
26
|
+
export {};
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { upsertSubscription } from "./subscriptions";
|
|
2
|
+
export async function syncPaddleEvent(event) {
|
|
3
|
+
const data = event.data;
|
|
4
|
+
const userId = data?.custom_data?.userId;
|
|
5
|
+
if (!userId) {
|
|
6
|
+
return {
|
|
7
|
+
synced: false,
|
|
8
|
+
reason: "missing userId in custom_data",
|
|
9
|
+
};
|
|
10
|
+
}
|
|
11
|
+
const plan = data?.custom_data?.plan ?? "starter";
|
|
12
|
+
if (event.event_type?.startsWith("subscription.")) {
|
|
13
|
+
await upsertSubscription({
|
|
14
|
+
userId,
|
|
15
|
+
plan,
|
|
16
|
+
status: data?.status === "active"
|
|
17
|
+
? "active"
|
|
18
|
+
: data?.status === "trialing"
|
|
19
|
+
? "trialing"
|
|
20
|
+
: data?.status === "paused"
|
|
21
|
+
? "paused"
|
|
22
|
+
: data?.status === "canceled"
|
|
23
|
+
? "canceled"
|
|
24
|
+
: "none",
|
|
25
|
+
paddleCustomerId: data?.customer_id,
|
|
26
|
+
paddleSubscriptionId: data?.id,
|
|
27
|
+
currentPeriodEnd: data?.current_billing_period
|
|
28
|
+
?.ends_at,
|
|
29
|
+
});
|
|
30
|
+
return {
|
|
31
|
+
synced: true,
|
|
32
|
+
type: "subscription",
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
return {
|
|
36
|
+
synced: false,
|
|
37
|
+
reason: "unhandled event type",
|
|
38
|
+
};
|
|
39
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function paddleApi<T>(path: string, options?: RequestInit): Promise<T>;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
const PADDLE_API_BASE = process.env.PADDLE_API_BASE ??
|
|
2
|
+
"https://api.paddle.com";
|
|
3
|
+
export async function paddleApi(path, options = {}) {
|
|
4
|
+
const apiKey = process.env.PADDLE_API_KEY;
|
|
5
|
+
if (!apiKey) {
|
|
6
|
+
throw new Error("Missing PADDLE_API_KEY");
|
|
7
|
+
}
|
|
8
|
+
const response = await fetch(`${PADDLE_API_BASE}${path}`, {
|
|
9
|
+
...options,
|
|
10
|
+
headers: {
|
|
11
|
+
Authorization: `Bearer ${apiKey}`,
|
|
12
|
+
"Content-Type": "application/json",
|
|
13
|
+
...options.headers,
|
|
14
|
+
},
|
|
15
|
+
});
|
|
16
|
+
if (!response.ok) {
|
|
17
|
+
const text = await response.text();
|
|
18
|
+
throw new Error(`Paddle API failed: ${response.status} ${text}`);
|
|
19
|
+
}
|
|
20
|
+
return response.json();
|
|
21
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function getPaddle(): Promise<import("@paddle/paddle-js").Paddle | undefined>;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { initializePaddle } from "@paddle/paddle-js";
|
|
2
|
+
let paddleInstance = null;
|
|
3
|
+
export async function getPaddle() {
|
|
4
|
+
if (paddleInstance) {
|
|
5
|
+
return paddleInstance;
|
|
6
|
+
}
|
|
7
|
+
paddleInstance =
|
|
8
|
+
await initializePaddle({
|
|
9
|
+
token: process.env
|
|
10
|
+
.NEXT_PUBLIC_PADDLE_CLIENT_TOKEN,
|
|
11
|
+
});
|
|
12
|
+
return paddleInstance;
|
|
13
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export interface PaddleCustomer {
|
|
2
|
+
id: string;
|
|
3
|
+
email?: string;
|
|
4
|
+
name?: string;
|
|
5
|
+
status?: string;
|
|
6
|
+
}
|
|
7
|
+
export interface PaddleCustomerListResponse {
|
|
8
|
+
data?: PaddleCustomer[];
|
|
9
|
+
}
|
|
10
|
+
export interface PaddleCustomerResponse {
|
|
11
|
+
data?: PaddleCustomer;
|
|
12
|
+
}
|
|
13
|
+
export declare function fetchPaddleCustomer(customerId: string): Promise<PaddleCustomerResponse>;
|
|
14
|
+
export declare function findPaddleCustomersByEmail(email: string): Promise<PaddleCustomerListResponse>;
|
|
15
|
+
export declare function findFirstPaddleCustomerByEmail(email: string): Promise<PaddleCustomer | null>;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { paddleApi } from "./api";
|
|
2
|
+
export async function fetchPaddleCustomer(customerId) {
|
|
3
|
+
return paddleApi(`/customers/${customerId}`);
|
|
4
|
+
}
|
|
5
|
+
export async function findPaddleCustomersByEmail(email) {
|
|
6
|
+
const params = new URLSearchParams({
|
|
7
|
+
email,
|
|
8
|
+
});
|
|
9
|
+
return paddleApi(`/customers?${params.toString()}`);
|
|
10
|
+
}
|
|
11
|
+
export async function findFirstPaddleCustomerByEmail(email) {
|
|
12
|
+
const response = await findPaddleCustomersByEmail(email);
|
|
13
|
+
return response.data?.[0] ?? null;
|
|
14
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export declare enum PaddleEventType {
|
|
2
|
+
TransactionCompleted = "transaction.completed",
|
|
3
|
+
TransactionPaid = "transaction.paid",
|
|
4
|
+
SubscriptionCreated = "subscription.created",
|
|
5
|
+
SubscriptionActivated = "subscription.activated",
|
|
6
|
+
SubscriptionUpdated = "subscription.updated",
|
|
7
|
+
SubscriptionCanceled = "subscription.canceled",
|
|
8
|
+
SubscriptionPaused = "subscription.paused",
|
|
9
|
+
CustomerCreated = "customer.created"
|
|
10
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export var PaddleEventType;
|
|
2
|
+
(function (PaddleEventType) {
|
|
3
|
+
PaddleEventType["TransactionCompleted"] = "transaction.completed";
|
|
4
|
+
PaddleEventType["TransactionPaid"] = "transaction.paid";
|
|
5
|
+
PaddleEventType["SubscriptionCreated"] = "subscription.created";
|
|
6
|
+
PaddleEventType["SubscriptionActivated"] = "subscription.activated";
|
|
7
|
+
PaddleEventType["SubscriptionUpdated"] = "subscription.updated";
|
|
8
|
+
PaddleEventType["SubscriptionCanceled"] = "subscription.canceled";
|
|
9
|
+
PaddleEventType["SubscriptionPaused"] = "subscription.paused";
|
|
10
|
+
PaddleEventType["CustomerCreated"] = "customer.created";
|
|
11
|
+
})(PaddleEventType || (PaddleEventType = {}));
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export interface CreatePortalSessionOptions {
|
|
2
|
+
customerId: string;
|
|
3
|
+
returnUrl?: string;
|
|
4
|
+
}
|
|
5
|
+
export interface PortalSessionResult {
|
|
6
|
+
url: string;
|
|
7
|
+
}
|
|
8
|
+
export declare function createCustomerPortalSession({ customerId, returnUrl, }: CreatePortalSessionOptions): Promise<PortalSessionResult>;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
export async function createCustomerPortalSession({ customerId, returnUrl, }) {
|
|
2
|
+
const apiKey = process.env.PADDLE_API_KEY;
|
|
3
|
+
if (!apiKey) {
|
|
4
|
+
throw new Error("Missing PADDLE_API_KEY");
|
|
5
|
+
}
|
|
6
|
+
const response = await fetch("https://api.paddle.com/customer-portal-sessions", {
|
|
7
|
+
method: "POST",
|
|
8
|
+
headers: {
|
|
9
|
+
Authorization: `Bearer ${apiKey}`,
|
|
10
|
+
"Content-Type": "application/json",
|
|
11
|
+
},
|
|
12
|
+
body: JSON.stringify({
|
|
13
|
+
customer_id: customerId,
|
|
14
|
+
urls: returnUrl
|
|
15
|
+
? { return_url: returnUrl }
|
|
16
|
+
: undefined,
|
|
17
|
+
}),
|
|
18
|
+
});
|
|
19
|
+
if (!response.ok) {
|
|
20
|
+
throw new Error("Failed to create Paddle customer portal session");
|
|
21
|
+
}
|
|
22
|
+
const data = (await response.json());
|
|
23
|
+
const url = data.data?.urls?.general;
|
|
24
|
+
if (!url) {
|
|
25
|
+
throw new Error("Paddle portal URL missing from response");
|
|
26
|
+
}
|
|
27
|
+
return { url };
|
|
28
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export interface PaddleSubscription {
|
|
2
|
+
id: string;
|
|
3
|
+
status: string;
|
|
4
|
+
customer_id?: string;
|
|
5
|
+
current_billing_period?: {
|
|
6
|
+
ends_at?: string;
|
|
7
|
+
};
|
|
8
|
+
custom_data?: {
|
|
9
|
+
userId?: string;
|
|
10
|
+
plan?: "free" | "starter" | "pro" | "business";
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
export interface PaddleSubscriptionResponse {
|
|
14
|
+
data?: PaddleSubscription;
|
|
15
|
+
}
|
|
16
|
+
export interface PaddleSubscriptionListResponse {
|
|
17
|
+
data?: PaddleSubscription[];
|
|
18
|
+
}
|
|
19
|
+
export declare function fetchPaddleSubscription(subscriptionId: string): Promise<PaddleSubscriptionResponse>;
|
|
20
|
+
export declare function fetchPaddleSubscriptionsForCustomer(customerId: string): Promise<PaddleSubscriptionListResponse>;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { paddleApi } from "./api";
|
|
2
|
+
export async function fetchPaddleSubscription(subscriptionId) {
|
|
3
|
+
return paddleApi(`/subscriptions/${subscriptionId}`);
|
|
4
|
+
}
|
|
5
|
+
export async function fetchPaddleSubscriptionsForCustomer(customerId) {
|
|
6
|
+
const params = new URLSearchParams({
|
|
7
|
+
customer_id: customerId,
|
|
8
|
+
});
|
|
9
|
+
return paddleApi(`/subscriptions?${params.toString()}`);
|
|
10
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function verifyPaddleWebhook(rawBody: string, signature: string): Promise<unknown>;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# Agency Use Case
|
|
2
|
+
|
|
3
|
+
Build SaaS billing systems faster.
|
|
4
|
+
|
|
5
|
+
## Functions
|
|
6
|
+
|
|
7
|
+
requireSubscription()
|
|
8
|
+
hasFeature()
|
|
9
|
+
canUse()
|
|
10
|
+
incrementUsage()
|
|
11
|
+
syncPaddleEvent()
|
|
12
|
+
|
|
13
|
+
## Ideal Clients
|
|
14
|
+
|
|
15
|
+
AI SaaS
|
|
16
|
+
Micro SaaS
|
|
17
|
+
Developer Tools
|
|
18
|
+
Membership Sites
|
|
19
|
+
API Products
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# Paddle SaaS Billing Starter
|
|
2
|
+
|
|
3
|
+
Quickstart documentation.
|
|
4
|
+
|
|
5
|
+
## Core Usage
|
|
6
|
+
|
|
7
|
+
import {
|
|
8
|
+
protectPlan,
|
|
9
|
+
protectFeature,
|
|
10
|
+
protectUsage
|
|
11
|
+
} from "@/lib/billing/protection";
|
|
12
|
+
|
|
13
|
+
await protectPlan(userId, "pro");
|
|
14
|
+
await protectFeature(userId, "api_access");
|
|
15
|
+
await protectUsage(userId, "ai_generation");
|
|
16
|
+
|
|
17
|
+
## Webhook
|
|
18
|
+
|
|
19
|
+
/api/paddle/webhook
|
|
20
|
+
|
|
21
|
+
## Environment
|
|
22
|
+
|
|
23
|
+
NEXT_PUBLIC_PADDLE_CLIENT_TOKEN=
|
|
24
|
+
PADDLE_WEBHOOK_SECRET=
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import type { BillingAdapter } from "@/lib/billing/adapters";
|
|
2
|
+
import { prisma } from "./client.example";
|
|
3
|
+
|
|
4
|
+
export const prismaBillingAdapter: BillingAdapter = {
|
|
5
|
+
async getSubscription(userId) {
|
|
6
|
+
return prisma.subscription.findUnique({
|
|
7
|
+
where: { userId },
|
|
8
|
+
});
|
|
9
|
+
},
|
|
10
|
+
|
|
11
|
+
async upsertSubscription(record) {
|
|
12
|
+
return prisma.subscription.upsert({
|
|
13
|
+
where: {
|
|
14
|
+
userId: record.userId,
|
|
15
|
+
},
|
|
16
|
+
create: record,
|
|
17
|
+
update: record,
|
|
18
|
+
});
|
|
19
|
+
},
|
|
20
|
+
|
|
21
|
+
async getUsage(
|
|
22
|
+
userId,
|
|
23
|
+
key,
|
|
24
|
+
period
|
|
25
|
+
) {
|
|
26
|
+
const usage =
|
|
27
|
+
await prisma.usageEvent.findUnique({
|
|
28
|
+
where: {
|
|
29
|
+
userId_key_period: {
|
|
30
|
+
userId,
|
|
31
|
+
key,
|
|
32
|
+
period,
|
|
33
|
+
},
|
|
34
|
+
},
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
return usage?.count ?? 0;
|
|
38
|
+
},
|
|
39
|
+
|
|
40
|
+
async incrementUsage(
|
|
41
|
+
userId,
|
|
42
|
+
key,
|
|
43
|
+
period,
|
|
44
|
+
amount
|
|
45
|
+
) {
|
|
46
|
+
const usage =
|
|
47
|
+
await prisma.usageEvent.upsert({
|
|
48
|
+
where: {
|
|
49
|
+
userId_key_period: {
|
|
50
|
+
userId,
|
|
51
|
+
key,
|
|
52
|
+
period,
|
|
53
|
+
},
|
|
54
|
+
},
|
|
55
|
+
create: {
|
|
56
|
+
userId,
|
|
57
|
+
key,
|
|
58
|
+
period,
|
|
59
|
+
count: amount,
|
|
60
|
+
},
|
|
61
|
+
update: {
|
|
62
|
+
count: {
|
|
63
|
+
increment: amount,
|
|
64
|
+
},
|
|
65
|
+
},
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
return usage.count;
|
|
69
|
+
},
|
|
70
|
+
};
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
model Subscription {
|
|
2
|
+
id String @id @default(cuid())
|
|
3
|
+
userId String @unique
|
|
4
|
+
|
|
5
|
+
plan String
|
|
6
|
+
status String
|
|
7
|
+
|
|
8
|
+
paddleCustomerId String?
|
|
9
|
+
paddleSubscriptionId String?
|
|
10
|
+
|
|
11
|
+
currentPeriodEnd DateTime?
|
|
12
|
+
|
|
13
|
+
createdAt DateTime @default(now())
|
|
14
|
+
updatedAt DateTime @updatedAt
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
model UsageEvent {
|
|
18
|
+
id String @id @default(cuid())
|
|
19
|
+
|
|
20
|
+
userId String
|
|
21
|
+
key String
|
|
22
|
+
period String
|
|
23
|
+
|
|
24
|
+
count Int @default(0)
|
|
25
|
+
|
|
26
|
+
createdAt DateTime @default(now())
|
|
27
|
+
updatedAt DateTime @updatedAt
|
|
28
|
+
|
|
29
|
+
@@unique([userId, key, period])
|
|
30
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "paddle-checkout-accelerator",
|
|
3
|
+
"version": "2.1.0",
|
|
4
|
+
"scripts": {
|
|
5
|
+
"dev": "next dev",
|
|
6
|
+
"build": "next build",
|
|
7
|
+
"start": "next start",
|
|
8
|
+
"lint": "eslint",
|
|
9
|
+
"test": "vitest run",
|
|
10
|
+
"build:package": "tsc -p tsconfig.package.json",
|
|
11
|
+
"prepack": "npm run build:package"
|
|
12
|
+
},
|
|
13
|
+
"dependencies": {
|
|
14
|
+
"@paddle/paddle-js": "^1.6.4",
|
|
15
|
+
"@radix-ui/react-dialog": "^1.1.16",
|
|
16
|
+
"@radix-ui/react-dropdown-menu": "^2.1.17",
|
|
17
|
+
"@radix-ui/react-scroll-area": "^1.2.11",
|
|
18
|
+
"@radix-ui/react-separator": "^1.1.9",
|
|
19
|
+
"@radix-ui/react-slot": "^1.2.5",
|
|
20
|
+
"@radix-ui/react-tabs": "^1.1.14",
|
|
21
|
+
"@radix-ui/react-tooltip": "^1.2.9",
|
|
22
|
+
"class-variance-authority": "^0.7.1",
|
|
23
|
+
"clsx": "^2.1.1",
|
|
24
|
+
"framer-motion": "^12.40.0",
|
|
25
|
+
"lucide-react": "^1.18.0",
|
|
26
|
+
"next": "16.2.9",
|
|
27
|
+
"react": "19.2.4",
|
|
28
|
+
"react-dom": "19.2.4",
|
|
29
|
+
"svix": "^1.95.2",
|
|
30
|
+
"tailwind-merge": "^3.6.0"
|
|
31
|
+
},
|
|
32
|
+
"devDependencies": {
|
|
33
|
+
"@tailwindcss/postcss": "^4",
|
|
34
|
+
"@types/node": "^20",
|
|
35
|
+
"@types/react": "^19",
|
|
36
|
+
"@types/react-dom": "^19",
|
|
37
|
+
"eslint": "^9",
|
|
38
|
+
"eslint-config-next": "16.2.9",
|
|
39
|
+
"tailwindcss": "^4",
|
|
40
|
+
"typescript": "^5",
|
|
41
|
+
"vitest": "^4.1.8"
|
|
42
|
+
},
|
|
43
|
+
"type": "module",
|
|
44
|
+
"main": "./dist/package/index.js",
|
|
45
|
+
"types": "./dist/package/index.d.ts",
|
|
46
|
+
"exports": {
|
|
47
|
+
".": {
|
|
48
|
+
"types": "./dist/package/index.d.ts",
|
|
49
|
+
"import": "./dist/package/index.js"
|
|
50
|
+
}
|
|
51
|
+
},
|
|
52
|
+
"files": [
|
|
53
|
+
"dist",
|
|
54
|
+
"README.md",
|
|
55
|
+
"LICENSE",
|
|
56
|
+
"docs",
|
|
57
|
+
"recipes"
|
|
58
|
+
]
|
|
59
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# Recipes
|
|
2
|
+
|
|
3
|
+
Copy-paste examples for real SaaS projects.
|
|
4
|
+
|
|
5
|
+
## Next.js
|
|
6
|
+
|
|
7
|
+
- protected-api-route.ts
|
|
8
|
+
- server-page-gate.tsx
|
|
9
|
+
- webhook-route.ts
|
|
10
|
+
- custom-adapter.example.txt
|
|
11
|
+
|
|
12
|
+
These recipes help agencies integrate Paddle billing into client projects quickly.
|
|
13
|
+
|
|
14
|
+
## Prisma
|
|
15
|
+
|
|
16
|
+
schema.prisma
|
|
17
|
+
adapter.example.txt
|
|
18
|
+
client.example.txt
|
|
19
|
+
|
|
20
|
+
Provides a production-ready adapter example.
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import {
|
|
2
|
+
configureBilling,
|
|
3
|
+
createPrismaBillingAdapter,
|
|
4
|
+
} from "@namahlogisticspaddle-checkout-accelerator";
|
|
5
|
+
|
|
6
|
+
import { prisma } from "@/lib/prisma";
|
|
7
|
+
|
|
8
|
+
const adapter =
|
|
9
|
+
createPrismaBillingAdapter(prisma);
|
|
10
|
+
|
|
11
|
+
export const billing =
|
|
12
|
+
configureBilling({
|
|
13
|
+
adapter,
|
|
14
|
+
});
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { BillingAdapter } from "@/lib/billing/adapters";
|
|
2
|
+
|
|
3
|
+
export const customBillingAdapter: BillingAdapter = {
|
|
4
|
+
async getSubscription(userId) {
|
|
5
|
+
// Replace with Prisma/Supabase query
|
|
6
|
+
console.log(userId);
|
|
7
|
+
return null;
|
|
8
|
+
},
|
|
9
|
+
|
|
10
|
+
async upsertSubscription(record) {
|
|
11
|
+
// Replace with Prisma/Supabase upsert
|
|
12
|
+
return record;
|
|
13
|
+
},
|
|
14
|
+
|
|
15
|
+
async getUsage(userId, key, period) {
|
|
16
|
+
// Replace with DB lookup
|
|
17
|
+
console.log(userId, key, period);
|
|
18
|
+
return 0;
|
|
19
|
+
},
|
|
20
|
+
|
|
21
|
+
async incrementUsage(userId, key, period, amount) {
|
|
22
|
+
// Replace with DB increment
|
|
23
|
+
console.log(userId, key, period, amount);
|
|
24
|
+
return amount;
|
|
25
|
+
},
|
|
26
|
+
};
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { NextResponse } from "next/server";
|
|
2
|
+
import { protectFeature, protectUsage } from "@/lib/billing/protection";
|
|
3
|
+
|
|
4
|
+
export async function POST() {
|
|
5
|
+
const userId = "replace-with-your-auth-user-id";
|
|
6
|
+
|
|
7
|
+
try {
|
|
8
|
+
await protectFeature(userId, "api_access");
|
|
9
|
+
await protectUsage(userId, "ai_generation");
|
|
10
|
+
|
|
11
|
+
return NextResponse.json({
|
|
12
|
+
success: true,
|
|
13
|
+
message: "Paid feature executed",
|
|
14
|
+
});
|
|
15
|
+
} catch (err) {
|
|
16
|
+
return NextResponse.json(
|
|
17
|
+
{
|
|
18
|
+
success: false,
|
|
19
|
+
error:
|
|
20
|
+
err instanceof Error
|
|
21
|
+
? err.message
|
|
22
|
+
: "Access denied",
|
|
23
|
+
},
|
|
24
|
+
{ status: 403 }
|
|
25
|
+
);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { redirect } from "next/navigation";
|
|
2
|
+
import { requireSubscription } from "@/lib/billing/subscriptions";
|
|
3
|
+
|
|
4
|
+
export default async function ProDashboardPage() {
|
|
5
|
+
const userId = "replace-with-your-auth-user-id";
|
|
6
|
+
|
|
7
|
+
try {
|
|
8
|
+
await requireSubscription(userId, "pro");
|
|
9
|
+
} catch {
|
|
10
|
+
redirect("/upgrade");
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
return (
|
|
14
|
+
<main>
|
|
15
|
+
<h1>Pro Dashboard</h1>
|
|
16
|
+
</main>
|
|
17
|
+
);
|
|
18
|
+
}
|