paddle-checkout-accelerator 2.3.0 → 2.5.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.
@@ -5,134 +5,206 @@ import path from "node:path";
5
5
  import prompts from "prompts";
6
6
 
7
7
  const cwd = process.cwd();
8
+ const args = new Set(process.argv.slice(2));
8
9
 
9
- function writeFileSafe(filePath, content, overwrite = false) {
10
- const absolutePath = path.join(cwd, filePath);
11
- fs.mkdirSync(path.dirname(absolutePath), { recursive: true });
10
+ const command =
11
+ process.argv[2];
12
12
 
13
- if (fs.existsSync(absolutePath) && !overwrite) {
14
- console.log(`Skipped ${filePath} because it already exists`);
13
+ const force =
14
+ args.has("--force");
15
+
16
+ const interactive =
17
+ args.has("--interactive") ||
18
+ args.has("-i");
19
+
20
+ const minimal =
21
+ args.has("--minimal");
22
+
23
+ function exists(filePath) {
24
+ return fs.existsSync(
25
+ path.join(cwd, filePath)
26
+ );
27
+ }
28
+
29
+ function detectAdapter() {
30
+ if (
31
+ exists("prisma/schema.prisma") ||
32
+ exists("src/lib/prisma.ts") ||
33
+ exists("lib/prisma.ts")
34
+ ) {
35
+ return "prisma";
36
+ }
37
+
38
+ return "memory";
39
+ }
40
+
41
+ function writeFileSafe(filePath, content) {
42
+ const absolutePath =
43
+ path.join(cwd, filePath);
44
+
45
+ const existed =
46
+ fs.existsSync(absolutePath);
47
+
48
+ fs.mkdirSync(
49
+ path.dirname(absolutePath),
50
+ { recursive: true }
51
+ );
52
+
53
+ if (existed && !force) {
54
+ console.log(
55
+ `Skipped ${filePath} because it already exists`
56
+ );
15
57
  return;
16
58
  }
17
59
 
18
- fs.writeFileSync(absolutePath, content);
19
- console.log(`${fs.existsSync(absolutePath) ? "Updated" : "Created"} ${filePath}`);
60
+ fs.writeFileSync(
61
+ absolutePath,
62
+ content
63
+ );
64
+
65
+ console.log(
66
+ `${existed ? "Updated" : "Created"} ${filePath}`
67
+ );
20
68
  }
21
69
 
22
70
  function appendEnvSafe(values) {
23
- const envPath = path.join(cwd, ".env.local");
24
- const content = Object.entries(values)
25
- .map(([key, value]) => `${key}=${value ?? ""}`)
26
- .join("\n");
71
+ const envPath =
72
+ path.join(cwd, ".env.local");
73
+
74
+ const content =
75
+ Object.entries(values)
76
+ .map(
77
+ ([key, value]) =>
78
+ `${key}=${value ?? ""}`
79
+ )
80
+ .join("\n");
27
81
 
28
82
  if (!fs.existsSync(envPath)) {
29
- fs.writeFileSync(envPath, content + "\n");
83
+ fs.writeFileSync(
84
+ envPath,
85
+ content + "\n"
86
+ );
87
+
30
88
  console.log("Created .env.local");
31
89
  return;
32
90
  }
33
91
 
34
- const current = fs.readFileSync(envPath, "utf8");
35
- const lines = content
36
- .split("\n")
37
- .filter((line) => {
38
- const key = line.split("=")[0];
39
- return !current.includes(`${key}=`);
40
- });
92
+ const current =
93
+ fs.readFileSync(envPath, "utf8");
94
+
95
+ const lines =
96
+ content
97
+ .split("\n")
98
+ .filter((line) => {
99
+ const key =
100
+ line.split("=")[0];
101
+
102
+ return !current.includes(
103
+ `${key}=`
104
+ );
105
+ });
41
106
 
42
107
  if (lines.length) {
43
- fs.appendFileSync(envPath, "\n" + lines.join("\n") + "\n");
108
+ fs.appendFileSync(
109
+ envPath,
110
+ "\n" + lines.join("\n") + "\n"
111
+ );
112
+
44
113
  console.log("Updated .env.local");
45
114
  } else {
46
- console.log("Skipped .env.local because Paddle keys already exist");
115
+ console.log(
116
+ "Skipped .env.local because Paddle keys already exist"
117
+ );
47
118
  }
48
119
  }
49
120
 
50
- async function main() {
51
- const command = process.argv[2];
52
-
53
- if (!command || command === "--help" || command === "-h") {
54
- console.log(`
55
- Paddle Checkout Accelerator
56
-
57
- Usage:
58
- npx paddle-checkout-accelerator init
59
- `);
60
- process.exit(0);
121
+ async function getConfig() {
122
+ const detectedAdapter =
123
+ detectAdapter();
124
+
125
+ if (!interactive) {
126
+ return {
127
+ adapter: detectedAdapter,
128
+ routes: true,
129
+ env: true,
130
+ schema:
131
+ detectedAdapter === "prisma",
132
+ ui: !minimal,
133
+ };
61
134
  }
62
135
 
63
- if (command !== "init") {
64
- console.error(`Unknown command: ${command}`);
65
- process.exit(1);
66
- }
67
-
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
- ]);
136
+ const answers =
137
+ await prompts([
138
+ {
139
+ type: "select",
140
+ name: "adapter",
141
+ message:
142
+ "Which storage adapter do you want?",
143
+ choices: [
144
+ {
145
+ title: "Auto-detect",
146
+ value: detectedAdapter,
147
+ },
148
+ {
149
+ title: "Memory demo adapter",
150
+ value: "memory",
151
+ },
152
+ {
153
+ title: "Prisma production adapter",
154
+ value: "prisma",
155
+ },
156
+ ],
157
+ initial: 0,
158
+ },
159
+ {
160
+ type: "toggle",
161
+ name: "routes",
162
+ message:
163
+ "Generate Paddle API routes?",
164
+ initial: true,
165
+ active: "yes",
166
+ inactive: "no",
167
+ },
168
+ {
169
+ type: "toggle",
170
+ name: "env",
171
+ message:
172
+ "Create/update .env.local placeholders?",
173
+ initial: true,
174
+ active: "yes",
175
+ inactive: "no",
176
+ },
177
+ {
178
+ type: "toggle",
179
+ name: "schema",
180
+ message:
181
+ "Generate Prisma schema example?",
182
+ initial:
183
+ detectedAdapter === "prisma",
184
+ active: "yes",
185
+ inactive: "no",
186
+ },
187
+ {
188
+ type: "toggle",
189
+ name: "ui",
190
+ message:
191
+ "Generate billing UI pages?",
192
+ initial: true,
193
+ active: "yes",
194
+ inactive: "no",
195
+ },
196
+ ]);
112
197
 
113
198
  if (!answers.adapter) {
114
199
  console.log("Install cancelled.");
115
200
  process.exit(0);
116
201
  }
117
202
 
118
- if (answers.adapter === "memory") {
119
- writeFileSafe(
120
- "src/lib/billing.ts",
121
- `import {
122
- configureBilling,
123
- memoryBillingAdapter,
124
- } from "paddle-checkout-accelerator";
125
-
126
- export const billing =
127
- configureBilling({
128
- adapter: memoryBillingAdapter,
129
- });
130
- `,
131
- answers.force
132
- );
133
- }
203
+ return answers;
204
+ }
134
205
 
135
- if (answers.adapter === "prisma") {
206
+ function writeBillingFile(adapter) {
207
+ if (adapter === "prisma") {
136
208
  writeFileSafe(
137
209
  "src/lib/billing.ts",
138
210
  `import {
@@ -149,8 +221,7 @@ export const billing =
149
221
  configureBilling({
150
222
  adapter,
151
223
  });
152
- `,
153
- answers.force
224
+ `
154
225
  );
155
226
 
156
227
  writeFileSafe(
@@ -169,14 +240,31 @@ export const prisma =
169
240
  if (process.env.NODE_ENV !== "production") {
170
241
  globalForPrisma.prisma = prisma;
171
242
  }
172
- `,
173
- answers.force
243
+ `
174
244
  );
175
245
 
176
- if (answers.schema) {
177
- writeFileSafe(
178
- "prisma/paddle-accelerator.schema.prisma",
179
- `model Subscription {
246
+ return;
247
+ }
248
+
249
+ writeFileSafe(
250
+ "src/lib/billing.ts",
251
+ `import {
252
+ configureBilling,
253
+ memoryBillingAdapter,
254
+ } from "paddle-checkout-accelerator";
255
+
256
+ export const billing =
257
+ configureBilling({
258
+ adapter: memoryBillingAdapter,
259
+ });
260
+ `
261
+ );
262
+ }
263
+
264
+ function writePrismaSchema() {
265
+ writeFileSafe(
266
+ "prisma/paddle-accelerator.schema.prisma",
267
+ `model Subscription {
180
268
  userId String @id
181
269
  plan String
182
270
  status String
@@ -210,16 +298,206 @@ model BillingEvent {
210
298
  createdAt DateTime
211
299
  metadata Json?
212
300
  }
213
- `,
214
- answers.force
301
+ `
302
+ );
303
+ }
304
+
305
+ function writeUiPages() {
306
+ writeFileSafe(
307
+ "src/app/billing/pricing/page.tsx",
308
+ `import {
309
+ PricingTable,
310
+ } from "paddle-checkout-accelerator";
311
+
312
+ export default function BillingPricingPage() {
313
+ return (
314
+ <main className="mx-auto max-w-5xl p-10">
315
+ <h1 className="text-4xl font-bold">
316
+ Choose your plan
317
+ </h1>
318
+
319
+ <p className="mt-3 text-gray-500">
320
+ Upgrade your account with Paddle billing.
321
+ </p>
322
+
323
+ <div className="mt-8">
324
+ <PricingTable />
325
+ </div>
326
+ </main>
327
+ );
328
+ }
329
+ `
330
+ );
331
+
332
+ writeFileSafe(
333
+ "src/app/billing/dashboard/page.tsx",
334
+ `import {
335
+ BillingHistory,
336
+ CustomerPortalButton,
337
+ SubscriptionCard,
338
+ TrialBanner,
339
+ UsageMeter,
340
+ } from "paddle-checkout-accelerator";
341
+
342
+ export default function BillingDashboardPage() {
343
+ return (
344
+ <main className="mx-auto max-w-5xl space-y-6 p-10">
345
+ <TrialBanner daysRemaining={5} />
346
+
347
+ <SubscriptionCard
348
+ plan="Pro"
349
+ status="Active"
350
+ />
351
+
352
+ <UsageMeter
353
+ current={37}
354
+ limit={50}
355
+ />
356
+
357
+ <CustomerPortalButton />
358
+
359
+ <BillingHistory />
360
+ </main>
361
+ );
362
+ }
363
+ `
364
+ );
365
+
366
+ writeFileSafe(
367
+ "src/app/billing/customer-repair/page.tsx",
368
+ `"use client";
369
+
370
+ import { useState } from "react";
371
+
372
+ export default function CustomerRepairPage() {
373
+ const [email, setEmail] =
374
+ useState("");
375
+ const [result, setResult] =
376
+ useState("");
377
+
378
+ async function repair() {
379
+ const response =
380
+ await fetch(
381
+ "/api/paddle/repair-by-email",
382
+ {
383
+ method: "POST",
384
+ headers: {
385
+ "Content-Type":
386
+ "application/json",
387
+ },
388
+ body: JSON.stringify({
389
+ email,
390
+ }),
391
+ }
215
392
  );
216
- }
393
+
394
+ const data =
395
+ await response.json();
396
+
397
+ setResult(
398
+ JSON.stringify(data, null, 2)
399
+ );
217
400
  }
218
401
 
219
- if (answers.routes) {
220
- writeFileSafe(
221
- "src/app/api/paddle/webhook/route.ts",
222
- `import { NextRequest, NextResponse } from "next/server";
402
+ return (
403
+ <main className="mx-auto max-w-3xl space-y-6 p-10">
404
+ <div>
405
+ <h1 className="text-4xl font-bold">
406
+ Repair Customer Access
407
+ </h1>
408
+
409
+ <p className="mt-3 text-gray-500">
410
+ Use this when a customer paid but their account did not unlock.
411
+ </p>
412
+ </div>
413
+
414
+ <input
415
+ value={email}
416
+ onChange={(event) =>
417
+ setEmail(event.target.value)
418
+ }
419
+ placeholder="customer@example.com"
420
+ className="w-full rounded-xl border p-4"
421
+ />
422
+
423
+ <button
424
+ onClick={repair}
425
+ className="rounded-xl bg-black px-6 py-3 text-white"
426
+ >
427
+ Repair Subscription
428
+ </button>
429
+
430
+ {result && (
431
+ <pre className="overflow-auto rounded-xl border bg-slate-50 p-5 text-sm">
432
+ {result}
433
+ </pre>
434
+ )}
435
+ </main>
436
+ );
437
+ }
438
+ `
439
+ );
440
+
441
+ writeFileSafe(
442
+ "src/app/billing/protected-api/page.tsx",
443
+ `"use client";
444
+
445
+ import { useState } from "react";
446
+
447
+ export default function ProtectedApiPage() {
448
+ const [result, setResult] =
449
+ useState("");
450
+
451
+ async function run() {
452
+ const response =
453
+ await fetch(
454
+ "/api/demo/protected-generation",
455
+ {
456
+ method: "POST",
457
+ }
458
+ );
459
+
460
+ const data =
461
+ await response.json();
462
+
463
+ setResult(
464
+ JSON.stringify(data, null, 2)
465
+ );
466
+ }
467
+
468
+ return (
469
+ <main className="mx-auto max-w-3xl space-y-6 p-10">
470
+ <h1 className="text-4xl font-bold">
471
+ Protected API Demo
472
+ </h1>
473
+
474
+ <p className="text-gray-500">
475
+ Test paid-route protection, feature checks, and usage limits.
476
+ </p>
477
+
478
+ <button
479
+ onClick={run}
480
+ className="rounded-xl bg-black px-6 py-3 text-white"
481
+ >
482
+ Run Protected Action
483
+ </button>
484
+
485
+ {result && (
486
+ <pre className="overflow-auto rounded-xl border bg-slate-50 p-5 text-sm">
487
+ {result}
488
+ </pre>
489
+ )}
490
+ </main>
491
+ );
492
+ }
493
+ `
494
+ );
495
+ }
496
+
497
+ function writeRoutes() {
498
+ writeFileSafe(
499
+ "src/app/api/paddle/webhook/route.ts",
500
+ `import { NextRequest, NextResponse } from "next/server";
223
501
  import {
224
502
  syncPaddleEvent,
225
503
  verifyPaddleWebhook,
@@ -230,94 +508,181 @@ export async function POST(request: NextRequest) {
230
508
  const signature = request.headers.get("paddle-signature");
231
509
 
232
510
  if (!signature) {
233
- return NextResponse.json({ error: "Missing Paddle signature" }, { status: 400 });
511
+ return NextResponse.json(
512
+ { error: "Missing Paddle signature" },
513
+ { status: 400 }
514
+ );
234
515
  }
235
516
 
236
517
  try {
237
- const event = await verifyPaddleWebhook(rawBody, signature);
518
+ const event =
519
+ await verifyPaddleWebhook(
520
+ rawBody,
521
+ signature
522
+ );
523
+
238
524
  await syncPaddleEvent(event);
239
- return NextResponse.json({ success: true });
525
+
526
+ return NextResponse.json({
527
+ success: true,
528
+ });
240
529
  } catch (error) {
241
530
  return NextResponse.json(
242
- { error: error instanceof Error ? error.message : "Webhook verification failed" },
531
+ {
532
+ error:
533
+ error instanceof Error
534
+ ? error.message
535
+ : "Webhook verification failed",
536
+ },
243
537
  { status: 401 }
244
538
  );
245
539
  }
246
540
  }
247
- `,
248
- answers.force
249
- );
541
+ `
542
+ );
250
543
 
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";
544
+ writeFileSafe(
545
+ "src/app/api/paddle/portal-session/route.ts",
546
+ `import { NextRequest, NextResponse } from "next/server";
547
+ import {
548
+ createCustomerPortalSession,
549
+ } from "paddle-checkout-accelerator";
255
550
 
256
551
  export async function POST(request: NextRequest) {
257
- const body = (await request.json()) as {
258
- customerId?: string;
259
- returnUrl?: string;
260
- };
552
+ const body =
553
+ (await request.json()) as {
554
+ customerId?: string;
555
+ returnUrl?: string;
556
+ };
261
557
 
262
558
  if (!body.customerId) {
263
- return NextResponse.json({ error: "Missing customerId" }, { status: 400 });
559
+ return NextResponse.json(
560
+ { error: "Missing customerId" },
561
+ { status: 400 }
562
+ );
264
563
  }
265
564
 
266
- const session = await createCustomerPortalSession({
267
- customerId: body.customerId,
268
- returnUrl: body.returnUrl,
269
- });
565
+ const session =
566
+ await createCustomerPortalSession({
567
+ customerId: body.customerId,
568
+ returnUrl: body.returnUrl,
569
+ });
270
570
 
271
571
  return NextResponse.json(session);
272
572
  }
273
- `,
274
- answers.force
275
- );
573
+ `
574
+ );
276
575
 
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";
576
+ writeFileSafe(
577
+ "src/app/api/paddle/refresh-subscription/route.ts",
578
+ `import { NextRequest, NextResponse } from "next/server";
579
+ import {
580
+ refreshSubscriptionFromPaddle,
581
+ } from "paddle-checkout-accelerator";
281
582
 
282
583
  export async function POST(request: NextRequest) {
283
- const body = (await request.json()) as {
284
- subscriptionId?: string;
285
- };
584
+ const body =
585
+ (await request.json()) as {
586
+ subscriptionId?: string;
587
+ };
286
588
 
287
589
  if (!body.subscriptionId) {
288
- return NextResponse.json({ error: "Missing subscriptionId" }, { status: 400 });
590
+ return NextResponse.json(
591
+ { error: "Missing subscriptionId" },
592
+ { status: 400 }
593
+ );
289
594
  }
290
595
 
291
- const subscription = await refreshSubscriptionFromPaddle(body.subscriptionId);
596
+ const subscription =
597
+ await refreshSubscriptionFromPaddle(
598
+ body.subscriptionId
599
+ );
600
+
292
601
  return NextResponse.json(subscription);
293
602
  }
294
- `,
295
- answers.force
296
- );
603
+ `
604
+ );
297
605
 
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";
606
+ writeFileSafe(
607
+ "src/app/api/paddle/repair-by-email/route.ts",
608
+ `import { NextRequest, NextResponse } from "next/server";
609
+ import {
610
+ repairSubscriptionByEmail,
611
+ } from "paddle-checkout-accelerator";
302
612
 
303
613
  export async function POST(request: NextRequest) {
304
- const body = (await request.json()) as {
305
- email?: string;
306
- };
614
+ const body =
615
+ (await request.json()) as {
616
+ email?: string;
617
+ };
307
618
 
308
619
  if (!body.email) {
309
- return NextResponse.json({ error: "Missing email" }, { status: 400 });
620
+ return NextResponse.json(
621
+ { error: "Missing email" },
622
+ { status: 400 }
623
+ );
310
624
  }
311
625
 
312
- const result = await repairSubscriptionByEmail(body.email);
626
+ const result =
627
+ await repairSubscriptionByEmail(
628
+ body.email
629
+ );
630
+
313
631
  return NextResponse.json(result);
314
632
  }
315
- `,
316
- answers.force
633
+ `
634
+ );
635
+ }
636
+
637
+ async function main() {
638
+ if (
639
+ !command ||
640
+ command === "--help" ||
641
+ command === "-h"
642
+ ) {
643
+ console.log(`
644
+ Paddle Checkout Accelerator
645
+
646
+ Usage:
647
+ npx paddle-checkout-accelerator init
648
+ npx paddle-checkout-accelerator init --interactive
649
+ npx paddle-checkout-accelerator init --force
650
+ npx paddle-checkout-accelerator init --minimal
651
+ `);
652
+ process.exit(0);
653
+ }
654
+
655
+ if (command !== "init") {
656
+ console.error(
657
+ `Unknown command: ${command}`
317
658
  );
659
+ process.exit(1);
318
660
  }
319
661
 
320
- if (answers.env) {
662
+ const config =
663
+ await getConfig();
664
+
665
+ console.log("");
666
+ console.log(
667
+ `Detected adapter: ${config.adapter}`
668
+ );
669
+ console.log("");
670
+
671
+ writeBillingFile(config.adapter);
672
+
673
+ if (config.schema) {
674
+ writePrismaSchema();
675
+ }
676
+
677
+ if (config.routes) {
678
+ writeRoutes();
679
+ }
680
+
681
+ if (config.ui) {
682
+ writeUiPages();
683
+ }
684
+
685
+ if (config.env) {
321
686
  appendEnvSafe({
322
687
  NEXT_PUBLIC_PADDLE_CLIENT_TOKEN: "",
323
688
  PADDLE_API_KEY: "",
@@ -326,12 +691,33 @@ export async function POST(request: NextRequest) {
326
691
  }
327
692
 
328
693
  console.log("");
329
- console.log("✅ Paddle Checkout Accelerator installed.");
694
+ console.log(
695
+ "✅ Paddle Checkout Accelerator installed."
696
+ );
330
697
  console.log("");
331
698
  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");
699
+ console.log(
700
+ "1. Add your Paddle keys to .env.local"
701
+ );
702
+
703
+ if (config.adapter === "prisma") {
704
+ console.log(
705
+ "2. Copy generated Prisma models into your main prisma/schema.prisma"
706
+ );
707
+ console.log(
708
+ "3. Run your Prisma migration"
709
+ );
710
+ console.log(
711
+ "4. Configure Paddle webhook URL: /api/paddle/webhook"
712
+ );
713
+ } else {
714
+ console.log(
715
+ "2. Use Prisma adapter for production"
716
+ );
717
+ console.log(
718
+ "3. Configure Paddle webhook URL: /api/paddle/webhook"
719
+ );
720
+ }
335
721
  }
336
722
 
337
723
  main().catch((error) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "paddle-checkout-accelerator",
3
- "version": "2.3.0",
3
+ "version": "2.5.0",
4
4
  "scripts": {
5
5
  "dev": "next dev",
6
6
  "build": "next build",