paddle-checkout-accelerator 2.4.0 → 2.6.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.
@@ -17,6 +17,9 @@ const interactive =
17
17
  args.has("--interactive") ||
18
18
  args.has("-i");
19
19
 
20
+ const minimal =
21
+ args.has("--minimal");
22
+
20
23
  function exists(filePath) {
21
24
  return fs.existsSync(
22
25
  path.join(cwd, filePath)
@@ -115,6 +118,120 @@ function appendEnvSafe(values) {
115
118
  }
116
119
  }
117
120
 
121
+ function readEnvFile() {
122
+ const envPath =
123
+ path.join(cwd, ".env.local");
124
+
125
+ if (!fs.existsSync(envPath)) {
126
+ return null;
127
+ }
128
+
129
+ return fs.readFileSync(envPath, "utf8");
130
+ }
131
+
132
+ function hasEnvValue(env, key) {
133
+ if (!env) return false;
134
+
135
+ const line =
136
+ env
137
+ .split("\n")
138
+ .find((entry) =>
139
+ entry.startsWith(`${key}=`)
140
+ );
141
+
142
+ if (!line) return false;
143
+
144
+ const value =
145
+ line.slice(key.length + 1).trim();
146
+
147
+ return value.length > 0;
148
+ }
149
+
150
+ function runDoctor() {
151
+ const env =
152
+ readEnvFile();
153
+
154
+ const checks = [
155
+ {
156
+ label: ".env.local exists",
157
+ pass: Boolean(env),
158
+ },
159
+ {
160
+ label: "PADDLE_API_KEY set",
161
+ pass: hasEnvValue(env, "PADDLE_API_KEY"),
162
+ },
163
+ {
164
+ label: "PADDLE_WEBHOOK_SECRET set",
165
+ pass: hasEnvValue(env, "PADDLE_WEBHOOK_SECRET"),
166
+ },
167
+ {
168
+ label: "NEXT_PUBLIC_PADDLE_CLIENT_TOKEN set",
169
+ pass: hasEnvValue(env, "NEXT_PUBLIC_PADDLE_CLIENT_TOKEN"),
170
+ },
171
+ {
172
+ label: "billing config exists",
173
+ pass: exists("src/lib/billing.ts"),
174
+ },
175
+ {
176
+ label: "webhook route exists",
177
+ pass: exists("src/app/api/paddle/webhook/route.ts"),
178
+ },
179
+ {
180
+ label: "portal route exists",
181
+ pass: exists("src/app/api/paddle/portal-session/route.ts"),
182
+ },
183
+ {
184
+ label: "refresh route exists",
185
+ pass: exists("src/app/api/paddle/refresh-subscription/route.ts"),
186
+ },
187
+ {
188
+ label: "repair route exists",
189
+ pass: exists("src/app/api/paddle/repair-by-email/route.ts"),
190
+ },
191
+ {
192
+ label: "pricing page exists",
193
+ pass: exists("src/app/billing/pricing/page.tsx"),
194
+ },
195
+ {
196
+ label: "billing dashboard exists",
197
+ pass: exists("src/app/billing/dashboard/page.tsx"),
198
+ },
199
+ {
200
+ label: "customer repair page exists",
201
+ pass: exists("src/app/billing/customer-repair/page.tsx"),
202
+ },
203
+ ];
204
+
205
+ console.log("");
206
+ console.log("Paddle Checkout Accelerator Doctor");
207
+ console.log("");
208
+
209
+ let failed = 0;
210
+
211
+ for (const check of checks) {
212
+ if (check.pass) {
213
+ console.log(`✅ ${check.label}`);
214
+ } else {
215
+ failed += 1;
216
+ console.log(`❌ ${check.label}`);
217
+ }
218
+ }
219
+
220
+ console.log("");
221
+
222
+ if (failed === 0) {
223
+ console.log("All checks passed.");
224
+ process.exit(0);
225
+ }
226
+
227
+ console.log(`${failed} check(s) failed.`);
228
+ console.log("");
229
+ console.log("Fix the missing items above, then run:");
230
+ console.log(" npx paddle-checkout-accelerator doctor");
231
+
232
+ process.exit(1);
233
+ }
234
+
118
235
  async function getConfig() {
119
236
  const detectedAdapter =
120
237
  detectAdapter();
@@ -126,6 +243,7 @@ async function getConfig() {
126
243
  env: true,
127
244
  schema:
128
245
  detectedAdapter === "prisma",
246
+ ui: !minimal,
129
247
  };
130
248
  }
131
249
 
@@ -180,6 +298,15 @@ async function getConfig() {
180
298
  active: "yes",
181
299
  inactive: "no",
182
300
  },
301
+ {
302
+ type: "toggle",
303
+ name: "ui",
304
+ message:
305
+ "Generate billing UI pages?",
306
+ initial: true,
307
+ active: "yes",
308
+ inactive: "no",
309
+ },
183
310
  ]);
184
311
 
185
312
  if (!answers.adapter) {
@@ -289,6 +416,198 @@ model BillingEvent {
289
416
  );
290
417
  }
291
418
 
419
+ function writeUiPages() {
420
+ writeFileSafe(
421
+ "src/app/billing/pricing/page.tsx",
422
+ `import {
423
+ PricingTable,
424
+ } from "paddle-checkout-accelerator";
425
+
426
+ export default function BillingPricingPage() {
427
+ return (
428
+ <main className="mx-auto max-w-5xl p-10">
429
+ <h1 className="text-4xl font-bold">
430
+ Choose your plan
431
+ </h1>
432
+
433
+ <p className="mt-3 text-gray-500">
434
+ Upgrade your account with Paddle billing.
435
+ </p>
436
+
437
+ <div className="mt-8">
438
+ <PricingTable />
439
+ </div>
440
+ </main>
441
+ );
442
+ }
443
+ `
444
+ );
445
+
446
+ writeFileSafe(
447
+ "src/app/billing/dashboard/page.tsx",
448
+ `import {
449
+ BillingHistory,
450
+ CustomerPortalButton,
451
+ SubscriptionCard,
452
+ TrialBanner,
453
+ UsageMeter,
454
+ } from "paddle-checkout-accelerator";
455
+
456
+ export default function BillingDashboardPage() {
457
+ return (
458
+ <main className="mx-auto max-w-5xl space-y-6 p-10">
459
+ <TrialBanner daysRemaining={5} />
460
+
461
+ <SubscriptionCard
462
+ plan="Pro"
463
+ status="Active"
464
+ />
465
+
466
+ <UsageMeter
467
+ current={37}
468
+ limit={50}
469
+ />
470
+
471
+ <CustomerPortalButton />
472
+
473
+ <BillingHistory />
474
+ </main>
475
+ );
476
+ }
477
+ `
478
+ );
479
+
480
+ writeFileSafe(
481
+ "src/app/billing/customer-repair/page.tsx",
482
+ `"use client";
483
+
484
+ import { useState } from "react";
485
+
486
+ export default function CustomerRepairPage() {
487
+ const [email, setEmail] =
488
+ useState("");
489
+ const [result, setResult] =
490
+ useState("");
491
+
492
+ async function repair() {
493
+ const response =
494
+ await fetch(
495
+ "/api/paddle/repair-by-email",
496
+ {
497
+ method: "POST",
498
+ headers: {
499
+ "Content-Type":
500
+ "application/json",
501
+ },
502
+ body: JSON.stringify({
503
+ email,
504
+ }),
505
+ }
506
+ );
507
+
508
+ const data =
509
+ await response.json();
510
+
511
+ setResult(
512
+ JSON.stringify(data, null, 2)
513
+ );
514
+ }
515
+
516
+ return (
517
+ <main className="mx-auto max-w-3xl space-y-6 p-10">
518
+ <div>
519
+ <h1 className="text-4xl font-bold">
520
+ Repair Customer Access
521
+ </h1>
522
+
523
+ <p className="mt-3 text-gray-500">
524
+ Use this when a customer paid but their account did not unlock.
525
+ </p>
526
+ </div>
527
+
528
+ <input
529
+ value={email}
530
+ onChange={(event) =>
531
+ setEmail(event.target.value)
532
+ }
533
+ placeholder="customer@example.com"
534
+ className="w-full rounded-xl border p-4"
535
+ />
536
+
537
+ <button
538
+ onClick={repair}
539
+ className="rounded-xl bg-black px-6 py-3 text-white"
540
+ >
541
+ Repair Subscription
542
+ </button>
543
+
544
+ {result && (
545
+ <pre className="overflow-auto rounded-xl border bg-slate-50 p-5 text-sm">
546
+ {result}
547
+ </pre>
548
+ )}
549
+ </main>
550
+ );
551
+ }
552
+ `
553
+ );
554
+
555
+ writeFileSafe(
556
+ "src/app/billing/protected-api/page.tsx",
557
+ `"use client";
558
+
559
+ import { useState } from "react";
560
+
561
+ export default function ProtectedApiPage() {
562
+ const [result, setResult] =
563
+ useState("");
564
+
565
+ async function run() {
566
+ const response =
567
+ await fetch(
568
+ "/api/demo/protected-generation",
569
+ {
570
+ method: "POST",
571
+ }
572
+ );
573
+
574
+ const data =
575
+ await response.json();
576
+
577
+ setResult(
578
+ JSON.stringify(data, null, 2)
579
+ );
580
+ }
581
+
582
+ return (
583
+ <main className="mx-auto max-w-3xl space-y-6 p-10">
584
+ <h1 className="text-4xl font-bold">
585
+ Protected API Demo
586
+ </h1>
587
+
588
+ <p className="text-gray-500">
589
+ Test paid-route protection, feature checks, and usage limits.
590
+ </p>
591
+
592
+ <button
593
+ onClick={run}
594
+ className="rounded-xl bg-black px-6 py-3 text-white"
595
+ >
596
+ Run Protected Action
597
+ </button>
598
+
599
+ {result && (
600
+ <pre className="overflow-auto rounded-xl border bg-slate-50 p-5 text-sm">
601
+ {result}
602
+ </pre>
603
+ )}
604
+ </main>
605
+ );
606
+ }
607
+ `
608
+ );
609
+ }
610
+
292
611
  function writeRoutes() {
293
612
  writeFileSafe(
294
613
  "src/app/api/paddle/webhook/route.ts",
@@ -442,10 +761,16 @@ Usage:
442
761
  npx paddle-checkout-accelerator init
443
762
  npx paddle-checkout-accelerator init --interactive
444
763
  npx paddle-checkout-accelerator init --force
764
+ npx paddle-checkout-accelerator doctor
765
+ npx paddle-checkout-accelerator init --minimal
445
766
  `);
446
767
  process.exit(0);
447
768
  }
448
769
 
770
+ if (command === "doctor") {
771
+ runDoctor();
772
+ }
773
+
449
774
  if (command !== "init") {
450
775
  console.error(
451
776
  `Unknown command: ${command}`
@@ -472,6 +797,10 @@ Usage:
472
797
  writeRoutes();
473
798
  }
474
799
 
800
+ if (config.ui) {
801
+ writeUiPages();
802
+ }
803
+
475
804
  if (config.env) {
476
805
  appendEnvSafe({
477
806
  NEXT_PUBLIC_PADDLE_CLIENT_TOKEN: "",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "paddle-checkout-accelerator",
3
- "version": "2.4.0",
3
+ "version": "2.6.0",
4
4
  "scripts": {
5
5
  "dev": "next dev",
6
6
  "build": "next build",