@codaijs/keel 0.2.2 → 0.2.4
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/dist/__tests__/sail-installer.test.js +25 -25
- package/dist/sail-installer.js +174 -174
- package/dist/scaffold.js +68 -68
- package/package.json +58 -58
- package/sails/_template/addon.json +20 -20
- package/sails/_template/install.ts +402 -402
- package/sails/admin-dashboard/README.md +117 -117
- package/sails/admin-dashboard/addon.json +28 -28
- package/sails/admin-dashboard/files/backend/middleware/admin.ts +34 -34
- package/sails/admin-dashboard/files/backend/routes/admin.ts +243 -243
- package/sails/admin-dashboard/files/frontend/components/admin/StatsCard.tsx +40 -40
- package/sails/admin-dashboard/files/frontend/components/admin/UsersTable.tsx +240 -240
- package/sails/admin-dashboard/files/frontend/hooks/useAdmin.ts +149 -149
- package/sails/admin-dashboard/files/frontend/pages/admin/Dashboard.tsx +173 -173
- package/sails/admin-dashboard/files/frontend/pages/admin/UserDetail.tsx +203 -203
- package/sails/admin-dashboard/install.ts +305 -305
- package/sails/analytics/README.md +178 -178
- package/sails/analytics/addon.json +27 -27
- package/sails/analytics/files/frontend/components/AnalyticsProvider.tsx +58 -58
- package/sails/analytics/files/frontend/hooks/useAnalytics.ts +64 -64
- package/sails/analytics/files/frontend/lib/analytics.ts +103 -103
- package/sails/analytics/install.ts +297 -297
- package/sails/file-uploads/addon.json +30 -30
- package/sails/file-uploads/files/backend/routes/files.ts +198 -198
- package/sails/file-uploads/files/backend/schema/files.ts +36 -36
- package/sails/file-uploads/files/backend/services/file-storage.ts +128 -128
- package/sails/file-uploads/files/frontend/components/FileList.tsx +248 -248
- package/sails/file-uploads/files/frontend/components/FileUploadButton.tsx +147 -147
- package/sails/file-uploads/files/frontend/hooks/useFileUpload.ts +106 -106
- package/sails/file-uploads/files/frontend/hooks/useFiles.ts +118 -118
- package/sails/file-uploads/files/frontend/pages/Files.tsx +37 -37
- package/sails/file-uploads/install.ts +466 -466
- package/sails/gdpr/README.md +174 -174
- package/sails/gdpr/addon.json +27 -27
- package/sails/gdpr/files/backend/routes/gdpr.ts +140 -140
- package/sails/gdpr/files/backend/services/gdpr.ts +293 -293
- package/sails/gdpr/files/frontend/components/auth/ConsentCheckboxes.tsx +97 -97
- package/sails/gdpr/files/frontend/components/gdpr/AccountDeletionRequest.tsx +192 -192
- package/sails/gdpr/files/frontend/components/gdpr/DataExportButton.tsx +75 -75
- package/sails/gdpr/files/frontend/pages/PrivacyPolicy.tsx +186 -186
- package/sails/gdpr/install.ts +756 -756
- package/sails/google-oauth/README.md +121 -121
- package/sails/google-oauth/addon.json +22 -22
- package/sails/google-oauth/files/GoogleButton.tsx +50 -50
- package/sails/google-oauth/install.ts +252 -252
- package/sails/i18n/README.md +193 -193
- package/sails/i18n/addon.json +30 -30
- package/sails/i18n/files/frontend/components/LanguageSwitcher.tsx +108 -108
- package/sails/i18n/files/frontend/hooks/useLanguage.ts +31 -31
- package/sails/i18n/files/frontend/lib/i18n.ts +32 -32
- package/sails/i18n/files/frontend/locales/de/common.json +44 -44
- package/sails/i18n/files/frontend/locales/en/common.json +44 -44
- package/sails/i18n/install.ts +407 -407
- package/sails/push-notifications/README.md +163 -163
- package/sails/push-notifications/addon.json +31 -31
- package/sails/push-notifications/files/backend/routes/notifications.ts +153 -153
- package/sails/push-notifications/files/backend/schema/notifications.ts +31 -31
- package/sails/push-notifications/files/backend/services/notifications.ts +117 -117
- package/sails/push-notifications/files/frontend/components/PushNotificationInit.tsx +12 -12
- package/sails/push-notifications/files/frontend/hooks/usePushNotifications.ts +154 -154
- package/sails/push-notifications/install.ts +384 -384
- package/sails/r2-storage/addon.json +29 -29
- package/sails/r2-storage/files/backend/services/storage.ts +71 -71
- package/sails/r2-storage/files/frontend/components/ProfilePictureUpload.tsx +167 -167
- package/sails/r2-storage/install.ts +412 -412
- package/sails/rate-limiting/addon.json +20 -20
- package/sails/rate-limiting/files/backend/middleware/rate-limit-store.ts +104 -104
- package/sails/rate-limiting/files/backend/middleware/rate-limit.ts +137 -137
- package/sails/rate-limiting/install.ts +300 -300
- package/sails/registry.json +107 -107
- package/sails/stripe/README.md +214 -214
- package/sails/stripe/addon.json +24 -24
- package/sails/stripe/files/backend/routes/stripe.ts +154 -154
- package/sails/stripe/files/backend/schema/stripe.ts +74 -74
- package/sails/stripe/files/backend/services/stripe.ts +224 -224
- package/sails/stripe/files/frontend/components/SubscriptionStatus.tsx +135 -135
- package/sails/stripe/files/frontend/hooks/useSubscription.ts +86 -86
- package/sails/stripe/files/frontend/pages/Checkout.tsx +116 -116
- package/sails/stripe/files/frontend/pages/Pricing.tsx +226 -226
- package/sails/stripe/install.ts +378 -378
package/sails/stripe/install.ts
CHANGED
|
@@ -1,378 +1,378 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Stripe Payments Sail Installer
|
|
3
|
-
*
|
|
4
|
-
* Adds Stripe subscription management with checkout sessions, webhooks,
|
|
5
|
-
* customer portal, and subscription status tracking.
|
|
6
|
-
* Features a full interactive setup wizard.
|
|
7
|
-
*
|
|
8
|
-
* Usage:
|
|
9
|
-
* npx tsx sails/stripe/install.ts
|
|
10
|
-
*/
|
|
11
|
-
|
|
12
|
-
import {
|
|
13
|
-
readFileSync,
|
|
14
|
-
writeFileSync,
|
|
15
|
-
copyFileSync,
|
|
16
|
-
existsSync,
|
|
17
|
-
mkdirSync,
|
|
18
|
-
} from "node:fs";
|
|
19
|
-
import { resolve, dirname, join } from "node:path";
|
|
20
|
-
import { execSync } from "node:child_process";
|
|
21
|
-
import { input, confirm } from "@inquirer/prompts";
|
|
22
|
-
|
|
23
|
-
// ---------------------------------------------------------------------------
|
|
24
|
-
// Paths
|
|
25
|
-
// ---------------------------------------------------------------------------
|
|
26
|
-
|
|
27
|
-
const SAIL_DIR = dirname(new URL(import.meta.url).pathname);
|
|
28
|
-
const PROJECT_ROOT = resolve(SAIL_DIR, "../..");
|
|
29
|
-
const BACKEND_ROOT = join(PROJECT_ROOT, "packages/backend");
|
|
30
|
-
const FRONTEND_ROOT = join(PROJECT_ROOT, "packages/frontend");
|
|
31
|
-
|
|
32
|
-
// ---------------------------------------------------------------------------
|
|
33
|
-
// Helpers
|
|
34
|
-
// ---------------------------------------------------------------------------
|
|
35
|
-
|
|
36
|
-
interface SailManifest {
|
|
37
|
-
name: string;
|
|
38
|
-
displayName: string;
|
|
39
|
-
version: string;
|
|
40
|
-
requiredEnvVars: { key: string; description: string }[];
|
|
41
|
-
dependencies: { backend: Record<string, string>; frontend: Record<string, string> };
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
function loadManifest(): SailManifest {
|
|
45
|
-
return JSON.parse(readFileSync(join(SAIL_DIR, "addon.json"), "utf-8"));
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
function insertAtMarker(filePath: string, marker: string, code: string): void {
|
|
49
|
-
if (!existsSync(filePath)) {
|
|
50
|
-
console.warn(` Warning: File not found: ${filePath}`);
|
|
51
|
-
return;
|
|
52
|
-
}
|
|
53
|
-
let content = readFileSync(filePath, "utf-8");
|
|
54
|
-
if (!content.includes(marker)) {
|
|
55
|
-
console.warn(` Warning: Marker "${marker}" not found in ${filePath}`);
|
|
56
|
-
return;
|
|
57
|
-
}
|
|
58
|
-
if (content.includes(code.trim())) {
|
|
59
|
-
console.log(` Skipped (already present) -> ${filePath}`);
|
|
60
|
-
return;
|
|
61
|
-
}
|
|
62
|
-
content = content.replace(marker, `${marker}\n${code}`);
|
|
63
|
-
writeFileSync(filePath, content, "utf-8");
|
|
64
|
-
console.log(` Modified -> ${filePath}`);
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
function copyFile(src: string, dest: string, label: string): void {
|
|
68
|
-
mkdirSync(dirname(dest), { recursive: true });
|
|
69
|
-
copyFileSync(src, dest);
|
|
70
|
-
console.log(` Copied -> ${label}`);
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
function appendToEnvExample(entries: Record<string, string>): void {
|
|
74
|
-
const envPath = join(PROJECT_ROOT, ".env.example");
|
|
75
|
-
if (!existsSync(envPath)) return;
|
|
76
|
-
let content = readFileSync(envPath, "utf-8");
|
|
77
|
-
const lines: string[] = [];
|
|
78
|
-
for (const [key, val] of Object.entries(entries)) {
|
|
79
|
-
if (!content.includes(key)) lines.push(`${key}=${val}`);
|
|
80
|
-
}
|
|
81
|
-
if (lines.length > 0) {
|
|
82
|
-
content += `\n# Stripe Payments\n${lines.join("\n")}\n`;
|
|
83
|
-
writeFileSync(envPath, content, "utf-8");
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
function installDeps(deps: Record<string, string>, workspace: string): void {
|
|
88
|
-
const entries = Object.entries(deps);
|
|
89
|
-
if (entries.length === 0) return;
|
|
90
|
-
const packages = entries.map(([n, v]) => `${n}@${v}`).join(" ");
|
|
91
|
-
const cmd = `npm install ${packages} --workspace=${workspace}`;
|
|
92
|
-
console.log(` Running: ${cmd}`);
|
|
93
|
-
execSync(cmd, { cwd: PROJECT_ROOT, stdio: "inherit" });
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
// ---------------------------------------------------------------------------
|
|
97
|
-
// Main
|
|
98
|
-
// ---------------------------------------------------------------------------
|
|
99
|
-
|
|
100
|
-
async function main(): Promise<void> {
|
|
101
|
-
const manifest = loadManifest();
|
|
102
|
-
|
|
103
|
-
// -- Step 1: Welcome message -------------------------------------------------
|
|
104
|
-
console.log("\n------------------------------------------------------------");
|
|
105
|
-
console.log(` Stripe Payments Sail Installer (v${manifest.version})`);
|
|
106
|
-
console.log("------------------------------------------------------------");
|
|
107
|
-
console.log();
|
|
108
|
-
console.log(" This sail integrates Stripe into your project, providing:");
|
|
109
|
-
console.log(" - Subscription management with checkout sessions");
|
|
110
|
-
console.log(" - Webhook handling for payment events");
|
|
111
|
-
console.log(" - Customer portal for self-service billing");
|
|
112
|
-
console.log(" - Subscription status tracking in the database");
|
|
113
|
-
console.log(" - Pricing page and checkout flow components");
|
|
114
|
-
console.log();
|
|
115
|
-
|
|
116
|
-
const pkgPath = join(PROJECT_ROOT, "package.json");
|
|
117
|
-
if (existsSync(pkgPath)) {
|
|
118
|
-
const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
|
|
119
|
-
console.log(` Template version: ${pkg.version ?? "unknown"}`);
|
|
120
|
-
console.log();
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
// -- Step 2: Stripe account check --------------------------------------------
|
|
124
|
-
const hasAccount = await confirm({
|
|
125
|
-
message: "Do you already have a Stripe account?",
|
|
126
|
-
default: true,
|
|
127
|
-
});
|
|
128
|
-
|
|
129
|
-
if (!hasAccount) {
|
|
130
|
-
console.log();
|
|
131
|
-
console.log(" Create a Stripe account at:");
|
|
132
|
-
console.log(" https://dashboard.stripe.com/register");
|
|
133
|
-
console.log();
|
|
134
|
-
console.log(" Stripe offers a generous test mode so you can develop");
|
|
135
|
-
console.log(" without processing real payments.");
|
|
136
|
-
console.log();
|
|
137
|
-
|
|
138
|
-
await confirm({
|
|
139
|
-
message: "I have created my Stripe account and am ready to continue",
|
|
140
|
-
default: false,
|
|
141
|
-
});
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
// -- Step 3: API keys -------------------------------------------------------
|
|
145
|
-
console.log();
|
|
146
|
-
console.log(" Get your API keys from:");
|
|
147
|
-
console.log(" https://dashboard.stripe.com/test/apikeys");
|
|
148
|
-
console.log();
|
|
149
|
-
|
|
150
|
-
const stripeSecretKey = await input({
|
|
151
|
-
message: "Stripe Secret Key:",
|
|
152
|
-
validate: (value) => {
|
|
153
|
-
if (!value || value.trim().length === 0) return "Secret Key is required.";
|
|
154
|
-
if (!value.startsWith("sk_")) return "Secret Key should start with 'sk_' (e.g., sk_test_...).";
|
|
155
|
-
return true;
|
|
156
|
-
},
|
|
157
|
-
});
|
|
158
|
-
|
|
159
|
-
const stripePublishableKey = await input({
|
|
160
|
-
message: "Stripe Publishable Key:",
|
|
161
|
-
validate: (value) => {
|
|
162
|
-
if (!value || value.trim().length === 0) return "Publishable Key is required.";
|
|
163
|
-
if (!value.startsWith("pk_")) return "Publishable Key should start with 'pk_' (e.g., pk_test_...).";
|
|
164
|
-
return true;
|
|
165
|
-
},
|
|
166
|
-
});
|
|
167
|
-
|
|
168
|
-
// -- Step 4: Webhook setup ---------------------------------------------------
|
|
169
|
-
console.log();
|
|
170
|
-
console.log(" Now set up your webhook endpoint:");
|
|
171
|
-
console.log();
|
|
172
|
-
console.log(" 1. Go to https://dashboard.stripe.com/test/webhooks");
|
|
173
|
-
console.log(' 2. Click "Add endpoint"');
|
|
174
|
-
console.log(" 3. Set the URL to:");
|
|
175
|
-
console.log(" {BACKEND_URL}/api/stripe/webhook");
|
|
176
|
-
console.log(" For local development:");
|
|
177
|
-
console.log(" http://localhost:3000/api/stripe/webhook");
|
|
178
|
-
console.log(" 4. Select these events:");
|
|
179
|
-
console.log(" - checkout.session.completed");
|
|
180
|
-
console.log(" - customer.subscription.updated");
|
|
181
|
-
console.log(" - customer.subscription.deleted");
|
|
182
|
-
console.log(" 5. Copy the signing secret (starts with whsec_)");
|
|
183
|
-
console.log();
|
|
184
|
-
|
|
185
|
-
const stripeWebhookSecret = await input({
|
|
186
|
-
message: "Stripe Webhook Secret:",
|
|
187
|
-
validate: (value) => {
|
|
188
|
-
if (!value || value.trim().length === 0) return "Webhook Secret is required.";
|
|
189
|
-
if (!value.startsWith("whsec_")) return "Webhook Secret should start with 'whsec_'.";
|
|
190
|
-
return true;
|
|
191
|
-
},
|
|
192
|
-
});
|
|
193
|
-
|
|
194
|
-
// -- Step 5: Product setup guidance ------------------------------------------
|
|
195
|
-
console.log();
|
|
196
|
-
const wantProducts = await confirm({
|
|
197
|
-
message: "Would you like guidance on creating test products now?",
|
|
198
|
-
default: false,
|
|
199
|
-
});
|
|
200
|
-
|
|
201
|
-
if (wantProducts) {
|
|
202
|
-
console.log();
|
|
203
|
-
console.log(" Create test products in the Stripe Dashboard:");
|
|
204
|
-
console.log();
|
|
205
|
-
console.log(" 1. Go to https://dashboard.stripe.com/test/products");
|
|
206
|
-
console.log(' 2. Click "Add product"');
|
|
207
|
-
console.log(" 3. Set a name (e.g., \"Pro Plan\") and description");
|
|
208
|
-
console.log(' 4. Under pricing, select "Recurring"');
|
|
209
|
-
console.log(" 5. Set the price (e.g., $19/month)");
|
|
210
|
-
console.log(' 6. Click "Save product"');
|
|
211
|
-
console.log(" 7. Copy the Price ID (price_...) for use in your Pricing.tsx");
|
|
212
|
-
console.log();
|
|
213
|
-
console.log(" Repeat for each plan (e.g., Basic, Pro, Enterprise).");
|
|
214
|
-
console.log();
|
|
215
|
-
|
|
216
|
-
await confirm({ message: "I have finished setting up products (or will do it later)", default: true });
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
// -- Step 6: Summary --------------------------------------------------------
|
|
220
|
-
console.log();
|
|
221
|
-
console.log(" Summary of changes:");
|
|
222
|
-
console.log(" -------------------");
|
|
223
|
-
console.log(" Files to copy (backend):");
|
|
224
|
-
console.log(" + packages/backend/src/db/schema/stripe.ts");
|
|
225
|
-
console.log(" + packages/backend/src/routes/stripe.ts");
|
|
226
|
-
console.log(" + packages/backend/src/services/stripe.ts");
|
|
227
|
-
console.log();
|
|
228
|
-
console.log(" Files to copy (frontend):");
|
|
229
|
-
console.log(" + packages/frontend/src/pages/Pricing.tsx");
|
|
230
|
-
console.log(" + packages/frontend/src/pages/Checkout.tsx");
|
|
231
|
-
console.log(" + packages/frontend/src/components/stripe/SubscriptionStatus.tsx");
|
|
232
|
-
console.log(" + packages/frontend/src/hooks/useSubscription.ts");
|
|
233
|
-
console.log();
|
|
234
|
-
console.log(" Files to modify:");
|
|
235
|
-
console.log(" ~ packages/backend/src/db/schema/index.ts");
|
|
236
|
-
console.log(" ~ packages/backend/src/index.ts");
|
|
237
|
-
console.log(" ~ packages/backend/src/env.ts");
|
|
238
|
-
console.log(" ~ packages/frontend/src/router.tsx");
|
|
239
|
-
console.log(" ~ .env.example / .env");
|
|
240
|
-
console.log();
|
|
241
|
-
console.log(" Environment variables:");
|
|
242
|
-
console.log(` STRIPE_SECRET_KEY=${stripeSecretKey.slice(0, 12)}...`);
|
|
243
|
-
console.log(` STRIPE_PUBLISHABLE_KEY=${stripePublishableKey.slice(0, 12)}...`);
|
|
244
|
-
console.log(` STRIPE_WEBHOOK_SECRET=${stripeWebhookSecret.slice(0, 12)}...`);
|
|
245
|
-
console.log();
|
|
246
|
-
|
|
247
|
-
// -- Step 7: Confirm and execute ---------------------------------------------
|
|
248
|
-
const proceed = await confirm({ message: "Proceed with installation?", default: true });
|
|
249
|
-
if (!proceed) { console.log("\n Installation cancelled.\n"); process.exit(0); }
|
|
250
|
-
|
|
251
|
-
console.log();
|
|
252
|
-
console.log(" Installing...");
|
|
253
|
-
console.log();
|
|
254
|
-
|
|
255
|
-
console.log(" Copying backend files...");
|
|
256
|
-
const backendFiles = [
|
|
257
|
-
{ src: "backend/schema/stripe.ts", dest: "src/db/schema/stripe.ts" },
|
|
258
|
-
{ src: "backend/routes/stripe.ts", dest: "src/routes/stripe.ts" },
|
|
259
|
-
{ src: "backend/services/stripe.ts", dest: "src/services/stripe.ts" },
|
|
260
|
-
];
|
|
261
|
-
for (const f of backendFiles) {
|
|
262
|
-
copyFile(join(SAIL_DIR, "files", f.src), join(BACKEND_ROOT, f.dest), f.dest);
|
|
263
|
-
}
|
|
264
|
-
|
|
265
|
-
console.log();
|
|
266
|
-
console.log(" Copying frontend files...");
|
|
267
|
-
const frontendFiles = [
|
|
268
|
-
{ src: "frontend/pages/Pricing.tsx", dest: "src/pages/Pricing.tsx" },
|
|
269
|
-
{ src: "frontend/pages/Checkout.tsx", dest: "src/pages/Checkout.tsx" },
|
|
270
|
-
{ src: "frontend/components/SubscriptionStatus.tsx", dest: "src/components/stripe/SubscriptionStatus.tsx" },
|
|
271
|
-
{ src: "frontend/hooks/useSubscription.ts", dest: "src/hooks/useSubscription.ts" },
|
|
272
|
-
];
|
|
273
|
-
for (const f of frontendFiles) {
|
|
274
|
-
copyFile(join(SAIL_DIR, "files", f.src), join(FRONTEND_ROOT, f.dest), f.dest);
|
|
275
|
-
}
|
|
276
|
-
|
|
277
|
-
console.log();
|
|
278
|
-
console.log(" Modifying backend files...");
|
|
279
|
-
|
|
280
|
-
insertAtMarker(join(BACKEND_ROOT, "src/db/schema/index.ts"), "// [SAIL_SCHEMA]", 'export * from "./stripe";');
|
|
281
|
-
insertAtMarker(join(BACKEND_ROOT, "src/index.ts"), "// [SAIL_IMPORTS]", 'import { stripeRouter } from "./routes/stripe";');
|
|
282
|
-
insertAtMarker(join(BACKEND_ROOT, "src/index.ts"), "// [SAIL_ROUTES]", 'app.use("/api/stripe", stripeRouter);');
|
|
283
|
-
|
|
284
|
-
// Stripe webhook needs the raw body — insert express.raw() BEFORE express.json()
|
|
285
|
-
const indexPath = join(BACKEND_ROOT, "src/index.ts");
|
|
286
|
-
if (existsSync(indexPath)) {
|
|
287
|
-
let indexContent = readFileSync(indexPath, "utf-8");
|
|
288
|
-
const rawMiddleware = 'app.use("/api/stripe/webhook", express.raw({ type: "application/json" }));';
|
|
289
|
-
if (!indexContent.includes(rawMiddleware)) {
|
|
290
|
-
indexContent = indexContent.replace(
|
|
291
|
-
"app.use(express.json());",
|
|
292
|
-
`// Raw body for Stripe webhook signature verification (must be before express.json())\n${rawMiddleware}\n\napp.use(express.json());`,
|
|
293
|
-
);
|
|
294
|
-
writeFileSync(indexPath, indexContent, "utf-8");
|
|
295
|
-
console.log(" Modified -> src/index.ts (added raw body middleware for webhook)");
|
|
296
|
-
}
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
insertAtMarker(
|
|
300
|
-
join(BACKEND_ROOT, "src/env.ts"),
|
|
301
|
-
"// [SAIL_ENV_VARS]",
|
|
302
|
-
` STRIPE_SECRET_KEY: z.string().min(1, "STRIPE_SECRET_KEY is required"),\n STRIPE_PUBLISHABLE_KEY: z.string().min(1, "STRIPE_PUBLISHABLE_KEY is required"),\n STRIPE_WEBHOOK_SECRET: z.string().min(1, "STRIPE_WEBHOOK_SECRET is required"),`
|
|
303
|
-
);
|
|
304
|
-
|
|
305
|
-
console.log();
|
|
306
|
-
console.log(" Modifying frontend files...");
|
|
307
|
-
|
|
308
|
-
const routerPath = join(FRONTEND_ROOT, "src/router.tsx");
|
|
309
|
-
insertAtMarker(routerPath, "// [SAIL_IMPORTS]", `import { PricingPage } from "./pages/Pricing";\nimport { CheckoutPage } from "./pages/Checkout";`);
|
|
310
|
-
insertAtMarker(
|
|
311
|
-
routerPath,
|
|
312
|
-
"{/* [SAIL_ROUTES] */}",
|
|
313
|
-
` {\n path: "/pricing",\n element: <PricingPage />,\n },\n {\n path: "/checkout/success",\n element: <CheckoutPage status="success" />,\n },\n {\n path: "/checkout/cancel",\n element: <CheckoutPage status="cancel" />,\n },`
|
|
314
|
-
);
|
|
315
|
-
|
|
316
|
-
console.log();
|
|
317
|
-
console.log(" Installing dependencies...");
|
|
318
|
-
installDeps(manifest.dependencies.backend, "packages/backend");
|
|
319
|
-
installDeps(manifest.dependencies.frontend, "packages/frontend");
|
|
320
|
-
|
|
321
|
-
console.log();
|
|
322
|
-
console.log(" Generating database migrations...");
|
|
323
|
-
try {
|
|
324
|
-
execSync("npx drizzle-kit generate", { cwd: BACKEND_ROOT, stdio: "inherit" });
|
|
325
|
-
} catch {
|
|
326
|
-
console.warn(" Warning: Could not generate migrations. Run manually: cd packages/backend && npx drizzle-kit generate");
|
|
327
|
-
}
|
|
328
|
-
|
|
329
|
-
console.log();
|
|
330
|
-
console.log(" Updating environment files...");
|
|
331
|
-
appendToEnvExample({ STRIPE_SECRET_KEY: stripeSecretKey, STRIPE_PUBLISHABLE_KEY: stripePublishableKey, STRIPE_WEBHOOK_SECRET: stripeWebhookSecret });
|
|
332
|
-
|
|
333
|
-
const dotEnvPath = join(PROJECT_ROOT, ".env");
|
|
334
|
-
if (existsSync(dotEnvPath)) {
|
|
335
|
-
let dotEnv = readFileSync(dotEnvPath, "utf-8");
|
|
336
|
-
if (!dotEnv.includes("STRIPE_SECRET_KEY")) {
|
|
337
|
-
dotEnv += `\n# Stripe Payments\nSTRIPE_SECRET_KEY=${stripeSecretKey}\nSTRIPE_PUBLISHABLE_KEY=${stripePublishableKey}\nSTRIPE_WEBHOOK_SECRET=${stripeWebhookSecret}\n`;
|
|
338
|
-
writeFileSync(dotEnvPath, dotEnv, "utf-8");
|
|
339
|
-
console.log(" Updated .env");
|
|
340
|
-
}
|
|
341
|
-
}
|
|
342
|
-
|
|
343
|
-
// -- Step 8: Print test instructions -----------------------------------------
|
|
344
|
-
console.log();
|
|
345
|
-
console.log("------------------------------------------------------------");
|
|
346
|
-
console.log(" Stripe Payments installed successfully!");
|
|
347
|
-
console.log("------------------------------------------------------------");
|
|
348
|
-
console.log();
|
|
349
|
-
console.log(" Next steps:");
|
|
350
|
-
console.log();
|
|
351
|
-
console.log(" 1. Run database migrations:");
|
|
352
|
-
console.log(" npm run db:migrate");
|
|
353
|
-
console.log();
|
|
354
|
-
console.log(" 2. Create products in Stripe Dashboard:");
|
|
355
|
-
console.log(" https://dashboard.stripe.com/test/products");
|
|
356
|
-
console.log(" Copy the price IDs (price_...) into your Pricing.tsx");
|
|
357
|
-
console.log();
|
|
358
|
-
console.log(" 3. Start your dev server:");
|
|
359
|
-
console.log(" npm run dev");
|
|
360
|
-
console.log();
|
|
361
|
-
console.log(" Testing locally with the Stripe CLI:");
|
|
362
|
-
console.log(" Install: brew install stripe/stripe-cli/stripe");
|
|
363
|
-
console.log(" Login: stripe login");
|
|
364
|
-
console.log(" Listen: stripe listen --forward-to localhost:3000/api/stripe/webhook");
|
|
365
|
-
console.log();
|
|
366
|
-
console.log(" Test card numbers:");
|
|
367
|
-
console.log(" Success: 4242 4242 4242 4242");
|
|
368
|
-
console.log(" Requires auth: 4000 0025 0000 3155");
|
|
369
|
-
console.log(" Declined: 4000 0000 0000 9995");
|
|
370
|
-
console.log(" Use any future expiry date, any CVC, and any postal code.");
|
|
371
|
-
console.log();
|
|
372
|
-
console.log(" Webhook testing:");
|
|
373
|
-
console.log(" Trigger events: stripe trigger checkout.session.completed");
|
|
374
|
-
console.log(" View events: stripe events list");
|
|
375
|
-
console.log();
|
|
376
|
-
}
|
|
377
|
-
|
|
378
|
-
main().catch((err) => { console.error("Installation failed:", err); process.exit(1); });
|
|
1
|
+
/**
|
|
2
|
+
* Stripe Payments Sail Installer
|
|
3
|
+
*
|
|
4
|
+
* Adds Stripe subscription management with checkout sessions, webhooks,
|
|
5
|
+
* customer portal, and subscription status tracking.
|
|
6
|
+
* Features a full interactive setup wizard.
|
|
7
|
+
*
|
|
8
|
+
* Usage:
|
|
9
|
+
* npx tsx sails/stripe/install.ts
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import {
|
|
13
|
+
readFileSync,
|
|
14
|
+
writeFileSync,
|
|
15
|
+
copyFileSync,
|
|
16
|
+
existsSync,
|
|
17
|
+
mkdirSync,
|
|
18
|
+
} from "node:fs";
|
|
19
|
+
import { resolve, dirname, join } from "node:path";
|
|
20
|
+
import { execSync } from "node:child_process";
|
|
21
|
+
import { input, confirm } from "@inquirer/prompts";
|
|
22
|
+
|
|
23
|
+
// ---------------------------------------------------------------------------
|
|
24
|
+
// Paths
|
|
25
|
+
// ---------------------------------------------------------------------------
|
|
26
|
+
|
|
27
|
+
const SAIL_DIR = dirname(new URL(import.meta.url).pathname);
|
|
28
|
+
const PROJECT_ROOT = resolve(SAIL_DIR, "../..");
|
|
29
|
+
const BACKEND_ROOT = join(PROJECT_ROOT, "packages/backend");
|
|
30
|
+
const FRONTEND_ROOT = join(PROJECT_ROOT, "packages/frontend");
|
|
31
|
+
|
|
32
|
+
// ---------------------------------------------------------------------------
|
|
33
|
+
// Helpers
|
|
34
|
+
// ---------------------------------------------------------------------------
|
|
35
|
+
|
|
36
|
+
interface SailManifest {
|
|
37
|
+
name: string;
|
|
38
|
+
displayName: string;
|
|
39
|
+
version: string;
|
|
40
|
+
requiredEnvVars: { key: string; description: string }[];
|
|
41
|
+
dependencies: { backend: Record<string, string>; frontend: Record<string, string> };
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function loadManifest(): SailManifest {
|
|
45
|
+
return JSON.parse(readFileSync(join(SAIL_DIR, "addon.json"), "utf-8"));
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function insertAtMarker(filePath: string, marker: string, code: string): void {
|
|
49
|
+
if (!existsSync(filePath)) {
|
|
50
|
+
console.warn(` Warning: File not found: ${filePath}`);
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
let content = readFileSync(filePath, "utf-8");
|
|
54
|
+
if (!content.includes(marker)) {
|
|
55
|
+
console.warn(` Warning: Marker "${marker}" not found in ${filePath}`);
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
if (content.includes(code.trim())) {
|
|
59
|
+
console.log(` Skipped (already present) -> ${filePath}`);
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
content = content.replace(marker, `${marker}\n${code}`);
|
|
63
|
+
writeFileSync(filePath, content, "utf-8");
|
|
64
|
+
console.log(` Modified -> ${filePath}`);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function copyFile(src: string, dest: string, label: string): void {
|
|
68
|
+
mkdirSync(dirname(dest), { recursive: true });
|
|
69
|
+
copyFileSync(src, dest);
|
|
70
|
+
console.log(` Copied -> ${label}`);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function appendToEnvExample(entries: Record<string, string>): void {
|
|
74
|
+
const envPath = join(PROJECT_ROOT, ".env.example");
|
|
75
|
+
if (!existsSync(envPath)) return;
|
|
76
|
+
let content = readFileSync(envPath, "utf-8");
|
|
77
|
+
const lines: string[] = [];
|
|
78
|
+
for (const [key, val] of Object.entries(entries)) {
|
|
79
|
+
if (!content.includes(key)) lines.push(`${key}=${val}`);
|
|
80
|
+
}
|
|
81
|
+
if (lines.length > 0) {
|
|
82
|
+
content += `\n# Stripe Payments\n${lines.join("\n")}\n`;
|
|
83
|
+
writeFileSync(envPath, content, "utf-8");
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function installDeps(deps: Record<string, string>, workspace: string): void {
|
|
88
|
+
const entries = Object.entries(deps);
|
|
89
|
+
if (entries.length === 0) return;
|
|
90
|
+
const packages = entries.map(([n, v]) => `${n}@${v}`).join(" ");
|
|
91
|
+
const cmd = `npm install ${packages} --workspace=${workspace}`;
|
|
92
|
+
console.log(` Running: ${cmd}`);
|
|
93
|
+
execSync(cmd, { cwd: PROJECT_ROOT, stdio: "inherit" });
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// ---------------------------------------------------------------------------
|
|
97
|
+
// Main
|
|
98
|
+
// ---------------------------------------------------------------------------
|
|
99
|
+
|
|
100
|
+
async function main(): Promise<void> {
|
|
101
|
+
const manifest = loadManifest();
|
|
102
|
+
|
|
103
|
+
// -- Step 1: Welcome message -------------------------------------------------
|
|
104
|
+
console.log("\n------------------------------------------------------------");
|
|
105
|
+
console.log(` Stripe Payments Sail Installer (v${manifest.version})`);
|
|
106
|
+
console.log("------------------------------------------------------------");
|
|
107
|
+
console.log();
|
|
108
|
+
console.log(" This sail integrates Stripe into your project, providing:");
|
|
109
|
+
console.log(" - Subscription management with checkout sessions");
|
|
110
|
+
console.log(" - Webhook handling for payment events");
|
|
111
|
+
console.log(" - Customer portal for self-service billing");
|
|
112
|
+
console.log(" - Subscription status tracking in the database");
|
|
113
|
+
console.log(" - Pricing page and checkout flow components");
|
|
114
|
+
console.log();
|
|
115
|
+
|
|
116
|
+
const pkgPath = join(PROJECT_ROOT, "package.json");
|
|
117
|
+
if (existsSync(pkgPath)) {
|
|
118
|
+
const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
|
|
119
|
+
console.log(` Template version: ${pkg.version ?? "unknown"}`);
|
|
120
|
+
console.log();
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// -- Step 2: Stripe account check --------------------------------------------
|
|
124
|
+
const hasAccount = await confirm({
|
|
125
|
+
message: "Do you already have a Stripe account?",
|
|
126
|
+
default: true,
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
if (!hasAccount) {
|
|
130
|
+
console.log();
|
|
131
|
+
console.log(" Create a Stripe account at:");
|
|
132
|
+
console.log(" https://dashboard.stripe.com/register");
|
|
133
|
+
console.log();
|
|
134
|
+
console.log(" Stripe offers a generous test mode so you can develop");
|
|
135
|
+
console.log(" without processing real payments.");
|
|
136
|
+
console.log();
|
|
137
|
+
|
|
138
|
+
await confirm({
|
|
139
|
+
message: "I have created my Stripe account and am ready to continue",
|
|
140
|
+
default: false,
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// -- Step 3: API keys -------------------------------------------------------
|
|
145
|
+
console.log();
|
|
146
|
+
console.log(" Get your API keys from:");
|
|
147
|
+
console.log(" https://dashboard.stripe.com/test/apikeys");
|
|
148
|
+
console.log();
|
|
149
|
+
|
|
150
|
+
const stripeSecretKey = await input({
|
|
151
|
+
message: "Stripe Secret Key:",
|
|
152
|
+
validate: (value) => {
|
|
153
|
+
if (!value || value.trim().length === 0) return "Secret Key is required.";
|
|
154
|
+
if (!value.startsWith("sk_")) return "Secret Key should start with 'sk_' (e.g., sk_test_...).";
|
|
155
|
+
return true;
|
|
156
|
+
},
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
const stripePublishableKey = await input({
|
|
160
|
+
message: "Stripe Publishable Key:",
|
|
161
|
+
validate: (value) => {
|
|
162
|
+
if (!value || value.trim().length === 0) return "Publishable Key is required.";
|
|
163
|
+
if (!value.startsWith("pk_")) return "Publishable Key should start with 'pk_' (e.g., pk_test_...).";
|
|
164
|
+
return true;
|
|
165
|
+
},
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
// -- Step 4: Webhook setup ---------------------------------------------------
|
|
169
|
+
console.log();
|
|
170
|
+
console.log(" Now set up your webhook endpoint:");
|
|
171
|
+
console.log();
|
|
172
|
+
console.log(" 1. Go to https://dashboard.stripe.com/test/webhooks");
|
|
173
|
+
console.log(' 2. Click "Add endpoint"');
|
|
174
|
+
console.log(" 3. Set the URL to:");
|
|
175
|
+
console.log(" {BACKEND_URL}/api/stripe/webhook");
|
|
176
|
+
console.log(" For local development:");
|
|
177
|
+
console.log(" http://localhost:3000/api/stripe/webhook");
|
|
178
|
+
console.log(" 4. Select these events:");
|
|
179
|
+
console.log(" - checkout.session.completed");
|
|
180
|
+
console.log(" - customer.subscription.updated");
|
|
181
|
+
console.log(" - customer.subscription.deleted");
|
|
182
|
+
console.log(" 5. Copy the signing secret (starts with whsec_)");
|
|
183
|
+
console.log();
|
|
184
|
+
|
|
185
|
+
const stripeWebhookSecret = await input({
|
|
186
|
+
message: "Stripe Webhook Secret:",
|
|
187
|
+
validate: (value) => {
|
|
188
|
+
if (!value || value.trim().length === 0) return "Webhook Secret is required.";
|
|
189
|
+
if (!value.startsWith("whsec_")) return "Webhook Secret should start with 'whsec_'.";
|
|
190
|
+
return true;
|
|
191
|
+
},
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
// -- Step 5: Product setup guidance ------------------------------------------
|
|
195
|
+
console.log();
|
|
196
|
+
const wantProducts = await confirm({
|
|
197
|
+
message: "Would you like guidance on creating test products now?",
|
|
198
|
+
default: false,
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
if (wantProducts) {
|
|
202
|
+
console.log();
|
|
203
|
+
console.log(" Create test products in the Stripe Dashboard:");
|
|
204
|
+
console.log();
|
|
205
|
+
console.log(" 1. Go to https://dashboard.stripe.com/test/products");
|
|
206
|
+
console.log(' 2. Click "Add product"');
|
|
207
|
+
console.log(" 3. Set a name (e.g., \"Pro Plan\") and description");
|
|
208
|
+
console.log(' 4. Under pricing, select "Recurring"');
|
|
209
|
+
console.log(" 5. Set the price (e.g., $19/month)");
|
|
210
|
+
console.log(' 6. Click "Save product"');
|
|
211
|
+
console.log(" 7. Copy the Price ID (price_...) for use in your Pricing.tsx");
|
|
212
|
+
console.log();
|
|
213
|
+
console.log(" Repeat for each plan (e.g., Basic, Pro, Enterprise).");
|
|
214
|
+
console.log();
|
|
215
|
+
|
|
216
|
+
await confirm({ message: "I have finished setting up products (or will do it later)", default: true });
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// -- Step 6: Summary --------------------------------------------------------
|
|
220
|
+
console.log();
|
|
221
|
+
console.log(" Summary of changes:");
|
|
222
|
+
console.log(" -------------------");
|
|
223
|
+
console.log(" Files to copy (backend):");
|
|
224
|
+
console.log(" + packages/backend/src/db/schema/stripe.ts");
|
|
225
|
+
console.log(" + packages/backend/src/routes/stripe.ts");
|
|
226
|
+
console.log(" + packages/backend/src/services/stripe.ts");
|
|
227
|
+
console.log();
|
|
228
|
+
console.log(" Files to copy (frontend):");
|
|
229
|
+
console.log(" + packages/frontend/src/pages/Pricing.tsx");
|
|
230
|
+
console.log(" + packages/frontend/src/pages/Checkout.tsx");
|
|
231
|
+
console.log(" + packages/frontend/src/components/stripe/SubscriptionStatus.tsx");
|
|
232
|
+
console.log(" + packages/frontend/src/hooks/useSubscription.ts");
|
|
233
|
+
console.log();
|
|
234
|
+
console.log(" Files to modify:");
|
|
235
|
+
console.log(" ~ packages/backend/src/db/schema/index.ts");
|
|
236
|
+
console.log(" ~ packages/backend/src/index.ts");
|
|
237
|
+
console.log(" ~ packages/backend/src/env.ts");
|
|
238
|
+
console.log(" ~ packages/frontend/src/router.tsx");
|
|
239
|
+
console.log(" ~ .env.example / .env");
|
|
240
|
+
console.log();
|
|
241
|
+
console.log(" Environment variables:");
|
|
242
|
+
console.log(` STRIPE_SECRET_KEY=${stripeSecretKey.slice(0, 12)}...`);
|
|
243
|
+
console.log(` STRIPE_PUBLISHABLE_KEY=${stripePublishableKey.slice(0, 12)}...`);
|
|
244
|
+
console.log(` STRIPE_WEBHOOK_SECRET=${stripeWebhookSecret.slice(0, 12)}...`);
|
|
245
|
+
console.log();
|
|
246
|
+
|
|
247
|
+
// -- Step 7: Confirm and execute ---------------------------------------------
|
|
248
|
+
const proceed = await confirm({ message: "Proceed with installation?", default: true });
|
|
249
|
+
if (!proceed) { console.log("\n Installation cancelled.\n"); process.exit(0); }
|
|
250
|
+
|
|
251
|
+
console.log();
|
|
252
|
+
console.log(" Installing...");
|
|
253
|
+
console.log();
|
|
254
|
+
|
|
255
|
+
console.log(" Copying backend files...");
|
|
256
|
+
const backendFiles = [
|
|
257
|
+
{ src: "backend/schema/stripe.ts", dest: "src/db/schema/stripe.ts" },
|
|
258
|
+
{ src: "backend/routes/stripe.ts", dest: "src/routes/stripe.ts" },
|
|
259
|
+
{ src: "backend/services/stripe.ts", dest: "src/services/stripe.ts" },
|
|
260
|
+
];
|
|
261
|
+
for (const f of backendFiles) {
|
|
262
|
+
copyFile(join(SAIL_DIR, "files", f.src), join(BACKEND_ROOT, f.dest), f.dest);
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
console.log();
|
|
266
|
+
console.log(" Copying frontend files...");
|
|
267
|
+
const frontendFiles = [
|
|
268
|
+
{ src: "frontend/pages/Pricing.tsx", dest: "src/pages/Pricing.tsx" },
|
|
269
|
+
{ src: "frontend/pages/Checkout.tsx", dest: "src/pages/Checkout.tsx" },
|
|
270
|
+
{ src: "frontend/components/SubscriptionStatus.tsx", dest: "src/components/stripe/SubscriptionStatus.tsx" },
|
|
271
|
+
{ src: "frontend/hooks/useSubscription.ts", dest: "src/hooks/useSubscription.ts" },
|
|
272
|
+
];
|
|
273
|
+
for (const f of frontendFiles) {
|
|
274
|
+
copyFile(join(SAIL_DIR, "files", f.src), join(FRONTEND_ROOT, f.dest), f.dest);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
console.log();
|
|
278
|
+
console.log(" Modifying backend files...");
|
|
279
|
+
|
|
280
|
+
insertAtMarker(join(BACKEND_ROOT, "src/db/schema/index.ts"), "// [SAIL_SCHEMA]", 'export * from "./stripe";');
|
|
281
|
+
insertAtMarker(join(BACKEND_ROOT, "src/index.ts"), "// [SAIL_IMPORTS]", 'import { stripeRouter } from "./routes/stripe";');
|
|
282
|
+
insertAtMarker(join(BACKEND_ROOT, "src/index.ts"), "// [SAIL_ROUTES]", 'app.use("/api/stripe", stripeRouter);');
|
|
283
|
+
|
|
284
|
+
// Stripe webhook needs the raw body — insert express.raw() BEFORE express.json()
|
|
285
|
+
const indexPath = join(BACKEND_ROOT, "src/index.ts");
|
|
286
|
+
if (existsSync(indexPath)) {
|
|
287
|
+
let indexContent = readFileSync(indexPath, "utf-8");
|
|
288
|
+
const rawMiddleware = 'app.use("/api/stripe/webhook", express.raw({ type: "application/json" }));';
|
|
289
|
+
if (!indexContent.includes(rawMiddleware)) {
|
|
290
|
+
indexContent = indexContent.replace(
|
|
291
|
+
"app.use(express.json());",
|
|
292
|
+
`// Raw body for Stripe webhook signature verification (must be before express.json())\n${rawMiddleware}\n\napp.use(express.json());`,
|
|
293
|
+
);
|
|
294
|
+
writeFileSync(indexPath, indexContent, "utf-8");
|
|
295
|
+
console.log(" Modified -> src/index.ts (added raw body middleware for webhook)");
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
insertAtMarker(
|
|
300
|
+
join(BACKEND_ROOT, "src/env.ts"),
|
|
301
|
+
"// [SAIL_ENV_VARS]",
|
|
302
|
+
` STRIPE_SECRET_KEY: z.string().min(1, "STRIPE_SECRET_KEY is required"),\n STRIPE_PUBLISHABLE_KEY: z.string().min(1, "STRIPE_PUBLISHABLE_KEY is required"),\n STRIPE_WEBHOOK_SECRET: z.string().min(1, "STRIPE_WEBHOOK_SECRET is required"),`
|
|
303
|
+
);
|
|
304
|
+
|
|
305
|
+
console.log();
|
|
306
|
+
console.log(" Modifying frontend files...");
|
|
307
|
+
|
|
308
|
+
const routerPath = join(FRONTEND_ROOT, "src/router.tsx");
|
|
309
|
+
insertAtMarker(routerPath, "// [SAIL_IMPORTS]", `import { PricingPage } from "./pages/Pricing";\nimport { CheckoutPage } from "./pages/Checkout";`);
|
|
310
|
+
insertAtMarker(
|
|
311
|
+
routerPath,
|
|
312
|
+
"{/* [SAIL_ROUTES] */}",
|
|
313
|
+
` {\n path: "/pricing",\n element: <PricingPage />,\n },\n {\n path: "/checkout/success",\n element: <CheckoutPage status="success" />,\n },\n {\n path: "/checkout/cancel",\n element: <CheckoutPage status="cancel" />,\n },`
|
|
314
|
+
);
|
|
315
|
+
|
|
316
|
+
console.log();
|
|
317
|
+
console.log(" Installing dependencies...");
|
|
318
|
+
installDeps(manifest.dependencies.backend, "packages/backend");
|
|
319
|
+
installDeps(manifest.dependencies.frontend, "packages/frontend");
|
|
320
|
+
|
|
321
|
+
console.log();
|
|
322
|
+
console.log(" Generating database migrations...");
|
|
323
|
+
try {
|
|
324
|
+
execSync("npx drizzle-kit generate", { cwd: BACKEND_ROOT, stdio: "inherit" });
|
|
325
|
+
} catch {
|
|
326
|
+
console.warn(" Warning: Could not generate migrations. Run manually: cd packages/backend && npx drizzle-kit generate");
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
console.log();
|
|
330
|
+
console.log(" Updating environment files...");
|
|
331
|
+
appendToEnvExample({ STRIPE_SECRET_KEY: stripeSecretKey, STRIPE_PUBLISHABLE_KEY: stripePublishableKey, STRIPE_WEBHOOK_SECRET: stripeWebhookSecret });
|
|
332
|
+
|
|
333
|
+
const dotEnvPath = join(PROJECT_ROOT, ".env");
|
|
334
|
+
if (existsSync(dotEnvPath)) {
|
|
335
|
+
let dotEnv = readFileSync(dotEnvPath, "utf-8");
|
|
336
|
+
if (!dotEnv.includes("STRIPE_SECRET_KEY")) {
|
|
337
|
+
dotEnv += `\n# Stripe Payments\nSTRIPE_SECRET_KEY=${stripeSecretKey}\nSTRIPE_PUBLISHABLE_KEY=${stripePublishableKey}\nSTRIPE_WEBHOOK_SECRET=${stripeWebhookSecret}\n`;
|
|
338
|
+
writeFileSync(dotEnvPath, dotEnv, "utf-8");
|
|
339
|
+
console.log(" Updated .env");
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
// -- Step 8: Print test instructions -----------------------------------------
|
|
344
|
+
console.log();
|
|
345
|
+
console.log("------------------------------------------------------------");
|
|
346
|
+
console.log(" Stripe Payments installed successfully!");
|
|
347
|
+
console.log("------------------------------------------------------------");
|
|
348
|
+
console.log();
|
|
349
|
+
console.log(" Next steps:");
|
|
350
|
+
console.log();
|
|
351
|
+
console.log(" 1. Run database migrations:");
|
|
352
|
+
console.log(" npm run db:migrate");
|
|
353
|
+
console.log();
|
|
354
|
+
console.log(" 2. Create products in Stripe Dashboard:");
|
|
355
|
+
console.log(" https://dashboard.stripe.com/test/products");
|
|
356
|
+
console.log(" Copy the price IDs (price_...) into your Pricing.tsx");
|
|
357
|
+
console.log();
|
|
358
|
+
console.log(" 3. Start your dev server:");
|
|
359
|
+
console.log(" npm run dev");
|
|
360
|
+
console.log();
|
|
361
|
+
console.log(" Testing locally with the Stripe CLI:");
|
|
362
|
+
console.log(" Install: brew install stripe/stripe-cli/stripe");
|
|
363
|
+
console.log(" Login: stripe login");
|
|
364
|
+
console.log(" Listen: stripe listen --forward-to localhost:3000/api/stripe/webhook");
|
|
365
|
+
console.log();
|
|
366
|
+
console.log(" Test card numbers:");
|
|
367
|
+
console.log(" Success: 4242 4242 4242 4242");
|
|
368
|
+
console.log(" Requires auth: 4000 0025 0000 3155");
|
|
369
|
+
console.log(" Declined: 4000 0000 0000 9995");
|
|
370
|
+
console.log(" Use any future expiry date, any CVC, and any postal code.");
|
|
371
|
+
console.log();
|
|
372
|
+
console.log(" Webhook testing:");
|
|
373
|
+
console.log(" Trigger events: stripe trigger checkout.session.completed");
|
|
374
|
+
console.log(" View events: stripe events list");
|
|
375
|
+
console.log();
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
main().catch((err) => { console.error("Installation failed:", err); process.exit(1); });
|