paddle-checkout-accelerator 2.2.0 → 2.3.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/README.md +2 -0
- package/bin/paddle-checkout-accelerator.js +240 -147
- package/package.json +4 -2
package/README.md
CHANGED
|
@@ -2,86 +2,123 @@
|
|
|
2
2
|
|
|
3
3
|
import fs from "node:fs";
|
|
4
4
|
import path from "node:path";
|
|
5
|
+
import prompts from "prompts";
|
|
5
6
|
|
|
6
7
|
const cwd = process.cwd();
|
|
7
8
|
|
|
8
|
-
function writeFileSafe(filePath, content) {
|
|
9
|
+
function writeFileSafe(filePath, content, overwrite = false) {
|
|
9
10
|
const absolutePath = path.join(cwd, filePath);
|
|
10
|
-
|
|
11
|
+
fs.mkdirSync(path.dirname(absolutePath), { recursive: true });
|
|
11
12
|
|
|
12
|
-
fs.
|
|
13
|
-
recursive: true,
|
|
14
|
-
});
|
|
15
|
-
|
|
16
|
-
if (fs.existsSync(absolutePath)) {
|
|
13
|
+
if (fs.existsSync(absolutePath) && !overwrite) {
|
|
17
14
|
console.log(`Skipped ${filePath} because it already exists`);
|
|
18
15
|
return;
|
|
19
16
|
}
|
|
20
17
|
|
|
21
18
|
fs.writeFileSync(absolutePath, content);
|
|
22
|
-
console.log(
|
|
19
|
+
console.log(`${fs.existsSync(absolutePath) ? "Updated" : "Created"} ${filePath}`);
|
|
23
20
|
}
|
|
24
21
|
|
|
25
|
-
function appendEnvSafe(
|
|
22
|
+
function appendEnvSafe(values) {
|
|
26
23
|
const envPath = path.join(cwd, ".env.local");
|
|
24
|
+
const content = Object.entries(values)
|
|
25
|
+
.map(([key, value]) => `${key}=${value ?? ""}`)
|
|
26
|
+
.join("\n");
|
|
27
27
|
|
|
28
28
|
if (!fs.existsSync(envPath)) {
|
|
29
|
-
fs.writeFileSync(envPath, content);
|
|
29
|
+
fs.writeFileSync(envPath, content + "\n");
|
|
30
30
|
console.log("Created .env.local");
|
|
31
31
|
return;
|
|
32
32
|
}
|
|
33
33
|
|
|
34
34
|
const current = fs.readFileSync(envPath, "utf8");
|
|
35
|
-
|
|
36
|
-
const nextLines = content
|
|
35
|
+
const lines = content
|
|
37
36
|
.split("\n")
|
|
38
|
-
.filter(Boolean)
|
|
39
37
|
.filter((line) => {
|
|
40
38
|
const key = line.split("=")[0];
|
|
41
39
|
return !current.includes(`${key}=`);
|
|
42
40
|
});
|
|
43
41
|
|
|
44
|
-
if (
|
|
42
|
+
if (lines.length) {
|
|
43
|
+
fs.appendFileSync(envPath, "\n" + lines.join("\n") + "\n");
|
|
44
|
+
console.log("Updated .env.local");
|
|
45
|
+
} else {
|
|
45
46
|
console.log("Skipped .env.local because Paddle keys already exist");
|
|
46
|
-
return;
|
|
47
47
|
}
|
|
48
|
-
|
|
49
|
-
fs.appendFileSync(
|
|
50
|
-
envPath,
|
|
51
|
-
`\n${nextLines.join("\n")}\n`
|
|
52
|
-
);
|
|
53
|
-
|
|
54
|
-
console.log("Updated .env.local");
|
|
55
48
|
}
|
|
56
49
|
|
|
57
|
-
|
|
50
|
+
async function main() {
|
|
51
|
+
const command = process.argv[2];
|
|
58
52
|
|
|
59
|
-
if (!command || command === "--help" || command === "-h") {
|
|
60
|
-
|
|
53
|
+
if (!command || command === "--help" || command === "-h") {
|
|
54
|
+
console.log(`
|
|
61
55
|
Paddle Checkout Accelerator
|
|
62
56
|
|
|
63
57
|
Usage:
|
|
64
58
|
npx paddle-checkout-accelerator init
|
|
65
|
-
|
|
66
|
-
This creates:
|
|
67
|
-
- src/lib/billing.ts
|
|
68
|
-
- src/app/api/paddle/webhook/route.ts
|
|
69
|
-
- src/app/api/paddle/portal-session/route.ts
|
|
70
|
-
- src/app/api/paddle/refresh-subscription/route.ts
|
|
71
|
-
- src/app/api/paddle/repair-by-email/route.ts
|
|
72
|
-
- .env.local placeholders
|
|
73
59
|
`);
|
|
74
|
-
|
|
75
|
-
}
|
|
60
|
+
process.exit(0);
|
|
61
|
+
}
|
|
76
62
|
|
|
77
|
-
if (command !== "init") {
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
}
|
|
63
|
+
if (command !== "init") {
|
|
64
|
+
console.error(`Unknown command: ${command}`);
|
|
65
|
+
process.exit(1);
|
|
66
|
+
}
|
|
81
67
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
68
|
+
const answers = await prompts([
|
|
69
|
+
{
|
|
70
|
+
type: "select",
|
|
71
|
+
name: "adapter",
|
|
72
|
+
message: "Which storage adapter do you want?",
|
|
73
|
+
choices: [
|
|
74
|
+
{ title: "Memory demo adapter", value: "memory" },
|
|
75
|
+
{ title: "Prisma production adapter", value: "prisma" },
|
|
76
|
+
],
|
|
77
|
+
initial: 0,
|
|
78
|
+
},
|
|
79
|
+
{
|
|
80
|
+
type: "toggle",
|
|
81
|
+
name: "routes",
|
|
82
|
+
message: "Generate Paddle API routes?",
|
|
83
|
+
initial: true,
|
|
84
|
+
active: "yes",
|
|
85
|
+
inactive: "no",
|
|
86
|
+
},
|
|
87
|
+
{
|
|
88
|
+
type: "toggle",
|
|
89
|
+
name: "env",
|
|
90
|
+
message: "Create/update .env.local placeholders?",
|
|
91
|
+
initial: true,
|
|
92
|
+
active: "yes",
|
|
93
|
+
inactive: "no",
|
|
94
|
+
},
|
|
95
|
+
{
|
|
96
|
+
type: "toggle",
|
|
97
|
+
name: "schema",
|
|
98
|
+
message: "Generate Prisma schema example?",
|
|
99
|
+
initial: true,
|
|
100
|
+
active: "yes",
|
|
101
|
+
inactive: "no",
|
|
102
|
+
},
|
|
103
|
+
{
|
|
104
|
+
type: "toggle",
|
|
105
|
+
name: "force",
|
|
106
|
+
message: "Overwrite existing generated files?",
|
|
107
|
+
initial: false,
|
|
108
|
+
active: "yes",
|
|
109
|
+
inactive: "no",
|
|
110
|
+
},
|
|
111
|
+
]);
|
|
112
|
+
|
|
113
|
+
if (!answers.adapter) {
|
|
114
|
+
console.log("Install cancelled.");
|
|
115
|
+
process.exit(0);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
if (answers.adapter === "memory") {
|
|
119
|
+
writeFileSafe(
|
|
120
|
+
"src/lib/billing.ts",
|
|
121
|
+
`import {
|
|
85
122
|
configureBilling,
|
|
86
123
|
memoryBillingAdapter,
|
|
87
124
|
} from "paddle-checkout-accelerator";
|
|
@@ -90,12 +127,99 @@ export const billing =
|
|
|
90
127
|
configureBilling({
|
|
91
128
|
adapter: memoryBillingAdapter,
|
|
92
129
|
});
|
|
93
|
-
|
|
94
|
-
|
|
130
|
+
`,
|
|
131
|
+
answers.force
|
|
132
|
+
);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
if (answers.adapter === "prisma") {
|
|
136
|
+
writeFileSafe(
|
|
137
|
+
"src/lib/billing.ts",
|
|
138
|
+
`import {
|
|
139
|
+
configureBilling,
|
|
140
|
+
createPrismaBillingAdapter,
|
|
141
|
+
} from "paddle-checkout-accelerator";
|
|
142
|
+
|
|
143
|
+
import { prisma } from "@/lib/prisma";
|
|
144
|
+
|
|
145
|
+
const adapter =
|
|
146
|
+
createPrismaBillingAdapter(prisma);
|
|
147
|
+
|
|
148
|
+
export const billing =
|
|
149
|
+
configureBilling({
|
|
150
|
+
adapter,
|
|
151
|
+
});
|
|
152
|
+
`,
|
|
153
|
+
answers.force
|
|
154
|
+
);
|
|
155
|
+
|
|
156
|
+
writeFileSafe(
|
|
157
|
+
"src/lib/prisma.ts",
|
|
158
|
+
`import { PrismaClient } from "@prisma/client";
|
|
159
|
+
|
|
160
|
+
const globalForPrisma =
|
|
161
|
+
globalThis as unknown as {
|
|
162
|
+
prisma?: PrismaClient;
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
export const prisma =
|
|
166
|
+
globalForPrisma.prisma ??
|
|
167
|
+
new PrismaClient();
|
|
168
|
+
|
|
169
|
+
if (process.env.NODE_ENV !== "production") {
|
|
170
|
+
globalForPrisma.prisma = prisma;
|
|
171
|
+
}
|
|
172
|
+
`,
|
|
173
|
+
answers.force
|
|
174
|
+
);
|
|
175
|
+
|
|
176
|
+
if (answers.schema) {
|
|
177
|
+
writeFileSafe(
|
|
178
|
+
"prisma/paddle-accelerator.schema.prisma",
|
|
179
|
+
`model Subscription {
|
|
180
|
+
userId String @id
|
|
181
|
+
plan String
|
|
182
|
+
status String
|
|
183
|
+
paddleCustomerId String?
|
|
184
|
+
paddleSubscriptionId String?
|
|
185
|
+
currentPeriodEnd String?
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
model UsageEvent {
|
|
189
|
+
id String @id @default(cuid())
|
|
190
|
+
userId String
|
|
191
|
+
key String
|
|
192
|
+
period String
|
|
193
|
+
count Int
|
|
95
194
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
195
|
+
@@unique([userId, key, period], name: "userId_key_period")
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
model Team {
|
|
199
|
+
teamId String @id
|
|
200
|
+
name String
|
|
201
|
+
plan String
|
|
202
|
+
ownerId String
|
|
203
|
+
members Json
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
model BillingEvent {
|
|
207
|
+
id String @id
|
|
208
|
+
userId String
|
|
209
|
+
type String
|
|
210
|
+
createdAt DateTime
|
|
211
|
+
metadata Json?
|
|
212
|
+
}
|
|
213
|
+
`,
|
|
214
|
+
answers.force
|
|
215
|
+
);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
if (answers.routes) {
|
|
220
|
+
writeFileSafe(
|
|
221
|
+
"src/app/api/paddle/webhook/route.ts",
|
|
222
|
+
`import { NextRequest, NextResponse } from "next/server";
|
|
99
223
|
import {
|
|
100
224
|
syncPaddleEvent,
|
|
101
225
|
verifyPaddleWebhook,
|
|
@@ -103,145 +227,114 @@ import {
|
|
|
103
227
|
|
|
104
228
|
export async function POST(request: NextRequest) {
|
|
105
229
|
const rawBody = await request.text();
|
|
106
|
-
|
|
107
|
-
const signature =
|
|
108
|
-
request.headers.get("paddle-signature");
|
|
230
|
+
const signature = request.headers.get("paddle-signature");
|
|
109
231
|
|
|
110
232
|
if (!signature) {
|
|
111
|
-
return NextResponse.json(
|
|
112
|
-
{ error: "Missing Paddle signature" },
|
|
113
|
-
{ status: 400 }
|
|
114
|
-
);
|
|
233
|
+
return NextResponse.json({ error: "Missing Paddle signature" }, { status: 400 });
|
|
115
234
|
}
|
|
116
235
|
|
|
117
236
|
try {
|
|
118
|
-
const event =
|
|
119
|
-
await verifyPaddleWebhook(
|
|
120
|
-
rawBody,
|
|
121
|
-
signature
|
|
122
|
-
);
|
|
123
|
-
|
|
237
|
+
const event = await verifyPaddleWebhook(rawBody, signature);
|
|
124
238
|
await syncPaddleEvent(event);
|
|
125
|
-
|
|
126
|
-
return NextResponse.json({
|
|
127
|
-
success: true,
|
|
128
|
-
});
|
|
239
|
+
return NextResponse.json({ success: true });
|
|
129
240
|
} catch (error) {
|
|
130
241
|
return NextResponse.json(
|
|
131
|
-
{
|
|
132
|
-
error:
|
|
133
|
-
error instanceof Error
|
|
134
|
-
? error.message
|
|
135
|
-
: "Webhook verification failed",
|
|
136
|
-
},
|
|
242
|
+
{ error: error instanceof Error ? error.message : "Webhook verification failed" },
|
|
137
243
|
{ status: 401 }
|
|
138
244
|
);
|
|
139
245
|
}
|
|
140
246
|
}
|
|
141
|
-
|
|
142
|
-
|
|
247
|
+
`,
|
|
248
|
+
answers.force
|
|
249
|
+
);
|
|
143
250
|
|
|
144
|
-
writeFileSafe(
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
import {
|
|
148
|
-
createCustomerPortalSession,
|
|
149
|
-
} from "paddle-checkout-accelerator";
|
|
251
|
+
writeFileSafe(
|
|
252
|
+
"src/app/api/paddle/portal-session/route.ts",
|
|
253
|
+
`import { NextRequest, NextResponse } from "next/server";
|
|
254
|
+
import { createCustomerPortalSession } from "paddle-checkout-accelerator";
|
|
150
255
|
|
|
151
256
|
export async function POST(request: NextRequest) {
|
|
152
|
-
const body =
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
};
|
|
257
|
+
const body = (await request.json()) as {
|
|
258
|
+
customerId?: string;
|
|
259
|
+
returnUrl?: string;
|
|
260
|
+
};
|
|
157
261
|
|
|
158
262
|
if (!body.customerId) {
|
|
159
|
-
return NextResponse.json(
|
|
160
|
-
{ error: "Missing customerId" },
|
|
161
|
-
{ status: 400 }
|
|
162
|
-
);
|
|
263
|
+
return NextResponse.json({ error: "Missing customerId" }, { status: 400 });
|
|
163
264
|
}
|
|
164
265
|
|
|
165
|
-
const session =
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
});
|
|
266
|
+
const session = await createCustomerPortalSession({
|
|
267
|
+
customerId: body.customerId,
|
|
268
|
+
returnUrl: body.returnUrl,
|
|
269
|
+
});
|
|
170
270
|
|
|
171
271
|
return NextResponse.json(session);
|
|
172
272
|
}
|
|
173
|
-
|
|
174
|
-
|
|
273
|
+
`,
|
|
274
|
+
answers.force
|
|
275
|
+
);
|
|
175
276
|
|
|
176
|
-
writeFileSafe(
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
import {
|
|
180
|
-
refreshSubscriptionFromPaddle,
|
|
181
|
-
} from "paddle-checkout-accelerator";
|
|
277
|
+
writeFileSafe(
|
|
278
|
+
"src/app/api/paddle/refresh-subscription/route.ts",
|
|
279
|
+
`import { NextRequest, NextResponse } from "next/server";
|
|
280
|
+
import { refreshSubscriptionFromPaddle } from "paddle-checkout-accelerator";
|
|
182
281
|
|
|
183
282
|
export async function POST(request: NextRequest) {
|
|
184
|
-
const body =
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
};
|
|
283
|
+
const body = (await request.json()) as {
|
|
284
|
+
subscriptionId?: string;
|
|
285
|
+
};
|
|
188
286
|
|
|
189
287
|
if (!body.subscriptionId) {
|
|
190
|
-
return NextResponse.json(
|
|
191
|
-
{ error: "Missing subscriptionId" },
|
|
192
|
-
{ status: 400 }
|
|
193
|
-
);
|
|
288
|
+
return NextResponse.json({ error: "Missing subscriptionId" }, { status: 400 });
|
|
194
289
|
}
|
|
195
290
|
|
|
196
|
-
const subscription =
|
|
197
|
-
await refreshSubscriptionFromPaddle(
|
|
198
|
-
body.subscriptionId
|
|
199
|
-
);
|
|
200
|
-
|
|
291
|
+
const subscription = await refreshSubscriptionFromPaddle(body.subscriptionId);
|
|
201
292
|
return NextResponse.json(subscription);
|
|
202
293
|
}
|
|
203
|
-
|
|
204
|
-
|
|
294
|
+
`,
|
|
295
|
+
answers.force
|
|
296
|
+
);
|
|
205
297
|
|
|
206
|
-
writeFileSafe(
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
import {
|
|
210
|
-
repairSubscriptionByEmail,
|
|
211
|
-
} from "paddle-checkout-accelerator";
|
|
298
|
+
writeFileSafe(
|
|
299
|
+
"src/app/api/paddle/repair-by-email/route.ts",
|
|
300
|
+
`import { NextRequest, NextResponse } from "next/server";
|
|
301
|
+
import { repairSubscriptionByEmail } from "paddle-checkout-accelerator";
|
|
212
302
|
|
|
213
303
|
export async function POST(request: NextRequest) {
|
|
214
|
-
const body =
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
};
|
|
304
|
+
const body = (await request.json()) as {
|
|
305
|
+
email?: string;
|
|
306
|
+
};
|
|
218
307
|
|
|
219
308
|
if (!body.email) {
|
|
220
|
-
return NextResponse.json(
|
|
221
|
-
{ error: "Missing email" },
|
|
222
|
-
{ status: 400 }
|
|
223
|
-
);
|
|
309
|
+
return NextResponse.json({ error: "Missing email" }, { status: 400 });
|
|
224
310
|
}
|
|
225
311
|
|
|
226
|
-
const result =
|
|
227
|
-
await repairSubscriptionByEmail(
|
|
228
|
-
body.email
|
|
229
|
-
);
|
|
230
|
-
|
|
312
|
+
const result = await repairSubscriptionByEmail(body.email);
|
|
231
313
|
return NextResponse.json(result);
|
|
232
314
|
}
|
|
233
|
-
|
|
234
|
-
|
|
315
|
+
`,
|
|
316
|
+
answers.force
|
|
317
|
+
);
|
|
318
|
+
}
|
|
235
319
|
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
320
|
+
if (answers.env) {
|
|
321
|
+
appendEnvSafe({
|
|
322
|
+
NEXT_PUBLIC_PADDLE_CLIENT_TOKEN: "",
|
|
323
|
+
PADDLE_API_KEY: "",
|
|
324
|
+
PADDLE_WEBHOOK_SECRET: "",
|
|
325
|
+
});
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
console.log("");
|
|
329
|
+
console.log("✅ Paddle Checkout Accelerator installed.");
|
|
330
|
+
console.log("");
|
|
331
|
+
console.log("Next:");
|
|
332
|
+
console.log("1. Add your Paddle keys to .env.local");
|
|
333
|
+
console.log("2. Add schema models to your main prisma/schema.prisma if using Prisma");
|
|
334
|
+
console.log("3. Configure Paddle webhook URL: /api/paddle/webhook");
|
|
335
|
+
}
|
|
240
336
|
|
|
241
|
-
|
|
242
|
-
console.
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
console.log("1. Add your Paddle keys to .env.local");
|
|
246
|
-
console.log("2. Replace memoryBillingAdapter with Prisma adapter for production");
|
|
247
|
-
console.log("3. Configure Paddle webhook URL: /api/paddle/webhook");
|
|
337
|
+
main().catch((error) => {
|
|
338
|
+
console.error(error);
|
|
339
|
+
process.exit(1);
|
|
340
|
+
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "paddle-checkout-accelerator",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.3.0",
|
|
4
4
|
"scripts": {
|
|
5
5
|
"dev": "next dev",
|
|
6
6
|
"build": "next build",
|
|
@@ -27,7 +27,8 @@
|
|
|
27
27
|
"react": "19.2.4",
|
|
28
28
|
"react-dom": "19.2.4",
|
|
29
29
|
"svix": "^1.95.2",
|
|
30
|
-
"tailwind-merge": "^3.6.0"
|
|
30
|
+
"tailwind-merge": "^3.6.0",
|
|
31
|
+
"prompts": "^2.4.2"
|
|
31
32
|
},
|
|
32
33
|
"devDependencies": {
|
|
33
34
|
"@tailwindcss/postcss": "^4",
|
|
@@ -36,6 +37,7 @@
|
|
|
36
37
|
"@types/react-dom": "^19",
|
|
37
38
|
"eslint": "^9",
|
|
38
39
|
"eslint-config-next": "16.2.9",
|
|
40
|
+
"prompts": "^2.4.2",
|
|
39
41
|
"tailwindcss": "^4",
|
|
40
42
|
"tsup": "^8.5.1",
|
|
41
43
|
"typescript": "^5",
|