@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
|
@@ -1,384 +1,384 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Push Notifications Sail Installer
|
|
3
|
-
*
|
|
4
|
-
* Adds push notification support via Capacitor + Firebase Cloud Messaging.
|
|
5
|
-
* Includes device token registration, server-side sending with firebase-admin,
|
|
6
|
-
* and a React hook for managing notification lifecycle.
|
|
7
|
-
*
|
|
8
|
-
* Usage:
|
|
9
|
-
* npx tsx sails/push-notifications/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 appendToEnvFiles(section: string, entries: Record<string, string>): void {
|
|
74
|
-
for (const envFile of [".env.example", ".env"]) {
|
|
75
|
-
const envPath = join(PROJECT_ROOT, envFile);
|
|
76
|
-
if (!existsSync(envPath)) continue;
|
|
77
|
-
let content = readFileSync(envPath, "utf-8");
|
|
78
|
-
const lines: string[] = [];
|
|
79
|
-
for (const [key, val] of Object.entries(entries)) {
|
|
80
|
-
if (!content.includes(key)) lines.push(`${key}=${val}`);
|
|
81
|
-
}
|
|
82
|
-
if (lines.length > 0) {
|
|
83
|
-
content += `\n# ${section}\n${lines.join("\n")}\n`;
|
|
84
|
-
writeFileSync(envPath, content, "utf-8");
|
|
85
|
-
console.log(` Updated ${envFile}`);
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
function installDeps(deps: Record<string, string>, workspace: string): void {
|
|
91
|
-
const entries = Object.entries(deps);
|
|
92
|
-
if (entries.length === 0) return;
|
|
93
|
-
const packages = entries.map(([n, v]) => `${n}@${v}`).join(" ");
|
|
94
|
-
const cmd = `npm install ${packages} --workspace=${workspace}`;
|
|
95
|
-
console.log(` Running: ${cmd}`);
|
|
96
|
-
execSync(cmd, { cwd: PROJECT_ROOT, stdio: "inherit" });
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
// ---------------------------------------------------------------------------
|
|
100
|
-
// Main
|
|
101
|
-
// ---------------------------------------------------------------------------
|
|
102
|
-
|
|
103
|
-
async function main(): Promise<void> {
|
|
104
|
-
const manifest = loadManifest();
|
|
105
|
-
|
|
106
|
-
// -- Step 1: Welcome --------------------------------------------------------
|
|
107
|
-
console.log("\n------------------------------------------------------------");
|
|
108
|
-
console.log(` Push Notifications Sail Installer (v${manifest.version})`);
|
|
109
|
-
console.log("------------------------------------------------------------");
|
|
110
|
-
console.log();
|
|
111
|
-
console.log(" This sail adds push notification support to your app:");
|
|
112
|
-
console.log(" - Firebase Cloud Messaging (FCM) for delivery");
|
|
113
|
-
console.log(" - Capacitor integration for native iOS/Android");
|
|
114
|
-
console.log(" - Device token registration and management");
|
|
115
|
-
console.log(" - Server-side notification sending with firebase-admin");
|
|
116
|
-
console.log(" - React hook for permission handling and token lifecycle");
|
|
117
|
-
console.log();
|
|
118
|
-
|
|
119
|
-
const pkgPath = join(PROJECT_ROOT, "package.json");
|
|
120
|
-
if (existsSync(pkgPath)) {
|
|
121
|
-
const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
|
|
122
|
-
console.log(` Template version: ${pkg.version ?? "unknown"}`);
|
|
123
|
-
console.log();
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
// -- Step 2: Firebase setup guide -------------------------------------------
|
|
127
|
-
const hasFirebase = await confirm({
|
|
128
|
-
message: "Do you already have a Firebase project with Cloud Messaging enabled?",
|
|
129
|
-
default: false,
|
|
130
|
-
});
|
|
131
|
-
|
|
132
|
-
if (!hasFirebase) {
|
|
133
|
-
console.log();
|
|
134
|
-
console.log(" Follow these steps to set up Firebase Cloud Messaging:");
|
|
135
|
-
console.log();
|
|
136
|
-
console.log(" 1. Go to https://console.firebase.google.com");
|
|
137
|
-
console.log(" 2. Click 'Add project' (or select an existing project)");
|
|
138
|
-
console.log(" 3. Follow the setup wizard (you can disable Google Analytics)");
|
|
139
|
-
console.log(" 4. In the project, go to Project Settings > Cloud Messaging");
|
|
140
|
-
console.log(" Make sure Cloud Messaging API (V1) is enabled");
|
|
141
|
-
console.log(" 5. Go to Project Settings > Service Accounts");
|
|
142
|
-
console.log(' 6. Click "Generate new private key" to download the JSON file');
|
|
143
|
-
console.log(" 7. Open the JSON file — you will need:");
|
|
144
|
-
console.log(" - project_id");
|
|
145
|
-
console.log(" - private_key (the full PEM string)");
|
|
146
|
-
console.log(" - client_email");
|
|
147
|
-
console.log();
|
|
148
|
-
|
|
149
|
-
await confirm({
|
|
150
|
-
message: "I have my Firebase project set up and service account JSON ready",
|
|
151
|
-
default: false,
|
|
152
|
-
});
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
// -- Step 3: Collect credentials --------------------------------------------
|
|
156
|
-
console.log();
|
|
157
|
-
console.log(" Enter your Firebase service account credentials.");
|
|
158
|
-
console.log(" These are found in the service account JSON file you downloaded.");
|
|
159
|
-
console.log();
|
|
160
|
-
|
|
161
|
-
const firebaseProjectId = await input({
|
|
162
|
-
message: "Firebase Project ID (project_id):",
|
|
163
|
-
validate: (value) => {
|
|
164
|
-
if (!value || value.trim().length === 0) return "Project ID is required.";
|
|
165
|
-
return true;
|
|
166
|
-
},
|
|
167
|
-
});
|
|
168
|
-
|
|
169
|
-
const firebaseClientEmail = await input({
|
|
170
|
-
message: "Firebase Client Email (client_email):",
|
|
171
|
-
validate: (value) => {
|
|
172
|
-
if (!value || value.trim().length === 0) return "Client email is required.";
|
|
173
|
-
if (!value.includes("@")) return "Should be a valid service account email.";
|
|
174
|
-
return true;
|
|
175
|
-
},
|
|
176
|
-
});
|
|
177
|
-
|
|
178
|
-
console.log();
|
|
179
|
-
console.log(" For the private key, paste the full PEM key from the JSON file.");
|
|
180
|
-
console.log(" It starts with -----BEGIN PRIVATE KEY----- and ends with -----END PRIVATE KEY-----");
|
|
181
|
-
console.log(" You can paste it as a single line with \\n for newlines.");
|
|
182
|
-
console.log();
|
|
183
|
-
|
|
184
|
-
const firebasePrivateKey = await input({
|
|
185
|
-
message: "Firebase Private Key (private_key):",
|
|
186
|
-
validate: (value) => {
|
|
187
|
-
if (!value || value.trim().length === 0) return "Private key is required.";
|
|
188
|
-
if (!value.includes("PRIVATE KEY")) return "Should contain 'PRIVATE KEY' — paste the full PEM key.";
|
|
189
|
-
return true;
|
|
190
|
-
},
|
|
191
|
-
});
|
|
192
|
-
|
|
193
|
-
// -- Step 4: Summary --------------------------------------------------------
|
|
194
|
-
console.log();
|
|
195
|
-
console.log(" Summary of changes:");
|
|
196
|
-
console.log(" -------------------");
|
|
197
|
-
console.log(" Files to copy (backend):");
|
|
198
|
-
console.log(" + packages/backend/src/db/schema/notifications.ts");
|
|
199
|
-
console.log(" + packages/backend/src/routes/notifications.ts");
|
|
200
|
-
console.log(" + packages/backend/src/services/notifications.ts");
|
|
201
|
-
console.log();
|
|
202
|
-
console.log(" Files to copy (frontend):");
|
|
203
|
-
console.log(" + packages/frontend/src/hooks/usePushNotifications.ts");
|
|
204
|
-
console.log(" + packages/frontend/src/components/PushNotificationInit.tsx");
|
|
205
|
-
console.log();
|
|
206
|
-
console.log(" Files to modify:");
|
|
207
|
-
console.log(" ~ packages/backend/src/db/schema/index.ts");
|
|
208
|
-
console.log(" ~ packages/backend/src/index.ts");
|
|
209
|
-
console.log(" ~ packages/backend/src/env.ts");
|
|
210
|
-
console.log(" ~ packages/frontend/src/components/layout/Layout.tsx");
|
|
211
|
-
console.log(" ~ .env.example / .env");
|
|
212
|
-
console.log();
|
|
213
|
-
console.log(" Environment variables:");
|
|
214
|
-
console.log(` FIREBASE_PROJECT_ID=${firebaseProjectId}`);
|
|
215
|
-
console.log(` FIREBASE_CLIENT_EMAIL=${firebaseClientEmail.slice(0, 20)}...`);
|
|
216
|
-
console.log(` FIREBASE_PRIVATE_KEY=-----BEGIN PRIVATE KEY-----...`);
|
|
217
|
-
console.log();
|
|
218
|
-
|
|
219
|
-
// -- Step 5: Confirm --------------------------------------------------------
|
|
220
|
-
const proceed = await confirm({ message: "Proceed with installation?", default: true });
|
|
221
|
-
if (!proceed) { console.log("\n Installation cancelled.\n"); process.exit(0); }
|
|
222
|
-
|
|
223
|
-
console.log();
|
|
224
|
-
console.log(" Installing...");
|
|
225
|
-
console.log();
|
|
226
|
-
|
|
227
|
-
// -- Step 6: Copy files and insert at markers --------------------------------
|
|
228
|
-
console.log(" Copying backend files...");
|
|
229
|
-
const backendFiles = [
|
|
230
|
-
{ src: "backend/schema/notifications.ts", dest: "src/db/schema/notifications.ts" },
|
|
231
|
-
{ src: "backend/routes/notifications.ts", dest: "src/routes/notifications.ts" },
|
|
232
|
-
{ src: "backend/services/notifications.ts", dest: "src/services/notifications.ts" },
|
|
233
|
-
];
|
|
234
|
-
for (const f of backendFiles) {
|
|
235
|
-
copyFile(join(SAIL_DIR, "files", f.src), join(BACKEND_ROOT, f.dest), f.dest);
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
console.log();
|
|
239
|
-
console.log(" Copying frontend files...");
|
|
240
|
-
const frontendFiles = [
|
|
241
|
-
{ src: "frontend/hooks/usePushNotifications.ts", dest: "src/hooks/usePushNotifications.ts" },
|
|
242
|
-
{ src: "frontend/components/PushNotificationInit.tsx", dest: "src/components/PushNotificationInit.tsx" },
|
|
243
|
-
];
|
|
244
|
-
for (const f of frontendFiles) {
|
|
245
|
-
copyFile(join(SAIL_DIR, "files", f.src), join(FRONTEND_ROOT, f.dest), f.dest);
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
console.log();
|
|
249
|
-
console.log(" Modifying backend files...");
|
|
250
|
-
|
|
251
|
-
insertAtMarker(
|
|
252
|
-
join(BACKEND_ROOT, "src/db/schema/index.ts"),
|
|
253
|
-
"// [SAIL_SCHEMA]",
|
|
254
|
-
'export * from "./notifications.js";',
|
|
255
|
-
);
|
|
256
|
-
|
|
257
|
-
insertAtMarker(
|
|
258
|
-
join(BACKEND_ROOT, "src/index.ts"),
|
|
259
|
-
"// [SAIL_IMPORTS]",
|
|
260
|
-
'import { notificationsRouter } from "./routes/notifications.js";',
|
|
261
|
-
);
|
|
262
|
-
|
|
263
|
-
insertAtMarker(
|
|
264
|
-
join(BACKEND_ROOT, "src/index.ts"),
|
|
265
|
-
"// [SAIL_ROUTES]",
|
|
266
|
-
'app.use("/api/notifications", notificationsRouter);',
|
|
267
|
-
);
|
|
268
|
-
|
|
269
|
-
insertAtMarker(
|
|
270
|
-
join(BACKEND_ROOT, "src/env.ts"),
|
|
271
|
-
"// [SAIL_ENV_VARS]",
|
|
272
|
-
` FIREBASE_PROJECT_ID: z.string().min(1, "FIREBASE_PROJECT_ID is required"),\n FIREBASE_PRIVATE_KEY: z.string().min(1, "FIREBASE_PRIVATE_KEY is required"),\n FIREBASE_CLIENT_EMAIL: z.string().min(1, "FIREBASE_CLIENT_EMAIL is required"),`,
|
|
273
|
-
);
|
|
274
|
-
|
|
275
|
-
console.log();
|
|
276
|
-
console.log(" Modifying frontend files...");
|
|
277
|
-
|
|
278
|
-
// Add PushNotificationInit to Layout.tsx
|
|
279
|
-
const layoutPath = join(FRONTEND_ROOT, "src/components/layout/Layout.tsx");
|
|
280
|
-
if (existsSync(layoutPath)) {
|
|
281
|
-
let layoutContent = readFileSync(layoutPath, "utf-8");
|
|
282
|
-
|
|
283
|
-
// Add import if not present
|
|
284
|
-
const importLine = 'import { PushNotificationInit } from "@/components/PushNotificationInit.js";';
|
|
285
|
-
if (!layoutContent.includes("PushNotificationInit")) {
|
|
286
|
-
// Insert import after the last import statement
|
|
287
|
-
const lastImportIndex = layoutContent.lastIndexOf("import ");
|
|
288
|
-
const lineEnd = layoutContent.indexOf("\n", lastImportIndex);
|
|
289
|
-
layoutContent =
|
|
290
|
-
layoutContent.slice(0, lineEnd + 1) +
|
|
291
|
-
importLine + "\n" +
|
|
292
|
-
layoutContent.slice(lineEnd + 1);
|
|
293
|
-
|
|
294
|
-
// Insert <PushNotificationInit /> after useDeepLinks() call
|
|
295
|
-
layoutContent = layoutContent.replace(
|
|
296
|
-
"useDeepLinks();",
|
|
297
|
-
"useDeepLinks();\n\n return (\n",
|
|
298
|
-
);
|
|
299
|
-
|
|
300
|
-
// Actually, let's just insert the component in the JSX
|
|
301
|
-
// Revert the above — re-read the file cleanly
|
|
302
|
-
layoutContent = readFileSync(layoutPath, "utf-8");
|
|
303
|
-
|
|
304
|
-
// Add import at the top after last import
|
|
305
|
-
const lastImport = layoutContent.lastIndexOf("import ");
|
|
306
|
-
const importLineEnd = layoutContent.indexOf("\n", lastImport);
|
|
307
|
-
layoutContent =
|
|
308
|
-
layoutContent.slice(0, importLineEnd + 1) +
|
|
309
|
-
importLine + "\n" +
|
|
310
|
-
layoutContent.slice(importLineEnd + 1);
|
|
311
|
-
|
|
312
|
-
// Add component in JSX — insert right after the opening <div>
|
|
313
|
-
layoutContent = layoutContent.replace(
|
|
314
|
-
'<div className="flex min-h-screen flex-col bg-keel-navy">',
|
|
315
|
-
'<div className="flex min-h-screen flex-col bg-keel-navy">\n <PushNotificationInit />',
|
|
316
|
-
);
|
|
317
|
-
|
|
318
|
-
writeFileSync(layoutPath, layoutContent, "utf-8");
|
|
319
|
-
console.log(` Modified -> Layout.tsx`);
|
|
320
|
-
} else {
|
|
321
|
-
console.log(` Skipped (already present) -> Layout.tsx`);
|
|
322
|
-
}
|
|
323
|
-
} else {
|
|
324
|
-
console.warn(" Warning: Layout.tsx not found. Add <PushNotificationInit /> manually.");
|
|
325
|
-
}
|
|
326
|
-
|
|
327
|
-
console.log();
|
|
328
|
-
console.log(" Installing dependencies...");
|
|
329
|
-
installDeps(manifest.dependencies.backend, "packages/backend");
|
|
330
|
-
installDeps(manifest.dependencies.frontend, "packages/frontend");
|
|
331
|
-
|
|
332
|
-
console.log();
|
|
333
|
-
console.log(" Generating database migrations...");
|
|
334
|
-
try {
|
|
335
|
-
execSync("npx drizzle-kit generate", { cwd: BACKEND_ROOT, stdio: "inherit" });
|
|
336
|
-
} catch {
|
|
337
|
-
console.warn(" Warning: Could not generate migrations. Run manually: cd packages/backend && npx drizzle-kit generate");
|
|
338
|
-
}
|
|
339
|
-
|
|
340
|
-
console.log();
|
|
341
|
-
console.log(" Updating environment files...");
|
|
342
|
-
appendToEnvFiles("Push Notifications (Firebase)", {
|
|
343
|
-
FIREBASE_PROJECT_ID: firebaseProjectId,
|
|
344
|
-
FIREBASE_PRIVATE_KEY: firebasePrivateKey,
|
|
345
|
-
FIREBASE_CLIENT_EMAIL: firebaseClientEmail,
|
|
346
|
-
});
|
|
347
|
-
|
|
348
|
-
// -- Step 7: Next steps ------------------------------------------------------
|
|
349
|
-
console.log();
|
|
350
|
-
console.log("------------------------------------------------------------");
|
|
351
|
-
console.log(" Push Notifications installed successfully!");
|
|
352
|
-
console.log("------------------------------------------------------------");
|
|
353
|
-
console.log();
|
|
354
|
-
console.log(" Next steps:");
|
|
355
|
-
console.log();
|
|
356
|
-
console.log(" 1. Run database migrations:");
|
|
357
|
-
console.log(" npm run db:migrate");
|
|
358
|
-
console.log();
|
|
359
|
-
console.log(" 2. Configure iOS (APNs):");
|
|
360
|
-
console.log(" - Go to https://developer.apple.com/account/resources/authkeys/list");
|
|
361
|
-
console.log(' - Create an APNs authentication key (check "Apple Push Notifications service")');
|
|
362
|
-
console.log(" - Download the .p8 key file");
|
|
363
|
-
console.log(" - In Firebase Console > Project Settings > Cloud Messaging > iOS:");
|
|
364
|
-
console.log(" Upload the APNs key, enter Key ID and Team ID");
|
|
365
|
-
console.log();
|
|
366
|
-
console.log(" 3. Configure Android:");
|
|
367
|
-
console.log(" - In Firebase Console, add an Android app to your project");
|
|
368
|
-
console.log(" - Download google-services.json");
|
|
369
|
-
console.log(" - Place it in your Android project: android/app/google-services.json");
|
|
370
|
-
console.log();
|
|
371
|
-
console.log(" 4. Sync Capacitor:");
|
|
372
|
-
console.log(" npx cap sync");
|
|
373
|
-
console.log();
|
|
374
|
-
console.log(" 5. Start your dev server:");
|
|
375
|
-
console.log(" npm run dev");
|
|
376
|
-
console.log();
|
|
377
|
-
console.log(" Testing push notifications:");
|
|
378
|
-
console.log(" - Push notifications only work on physical devices (not simulators)");
|
|
379
|
-
console.log(" - Use the Firebase Console > Messaging to send test notifications");
|
|
380
|
-
console.log(" - Or use the POST /api/notifications/send endpoint from your backend");
|
|
381
|
-
console.log();
|
|
382
|
-
}
|
|
383
|
-
|
|
384
|
-
main().catch((err) => { console.error("Installation failed:", err); process.exit(1); });
|
|
1
|
+
/**
|
|
2
|
+
* Push Notifications Sail Installer
|
|
3
|
+
*
|
|
4
|
+
* Adds push notification support via Capacitor + Firebase Cloud Messaging.
|
|
5
|
+
* Includes device token registration, server-side sending with firebase-admin,
|
|
6
|
+
* and a React hook for managing notification lifecycle.
|
|
7
|
+
*
|
|
8
|
+
* Usage:
|
|
9
|
+
* npx tsx sails/push-notifications/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 appendToEnvFiles(section: string, entries: Record<string, string>): void {
|
|
74
|
+
for (const envFile of [".env.example", ".env"]) {
|
|
75
|
+
const envPath = join(PROJECT_ROOT, envFile);
|
|
76
|
+
if (!existsSync(envPath)) continue;
|
|
77
|
+
let content = readFileSync(envPath, "utf-8");
|
|
78
|
+
const lines: string[] = [];
|
|
79
|
+
for (const [key, val] of Object.entries(entries)) {
|
|
80
|
+
if (!content.includes(key)) lines.push(`${key}=${val}`);
|
|
81
|
+
}
|
|
82
|
+
if (lines.length > 0) {
|
|
83
|
+
content += `\n# ${section}\n${lines.join("\n")}\n`;
|
|
84
|
+
writeFileSync(envPath, content, "utf-8");
|
|
85
|
+
console.log(` Updated ${envFile}`);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function installDeps(deps: Record<string, string>, workspace: string): void {
|
|
91
|
+
const entries = Object.entries(deps);
|
|
92
|
+
if (entries.length === 0) return;
|
|
93
|
+
const packages = entries.map(([n, v]) => `${n}@${v}`).join(" ");
|
|
94
|
+
const cmd = `npm install ${packages} --workspace=${workspace}`;
|
|
95
|
+
console.log(` Running: ${cmd}`);
|
|
96
|
+
execSync(cmd, { cwd: PROJECT_ROOT, stdio: "inherit" });
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// ---------------------------------------------------------------------------
|
|
100
|
+
// Main
|
|
101
|
+
// ---------------------------------------------------------------------------
|
|
102
|
+
|
|
103
|
+
async function main(): Promise<void> {
|
|
104
|
+
const manifest = loadManifest();
|
|
105
|
+
|
|
106
|
+
// -- Step 1: Welcome --------------------------------------------------------
|
|
107
|
+
console.log("\n------------------------------------------------------------");
|
|
108
|
+
console.log(` Push Notifications Sail Installer (v${manifest.version})`);
|
|
109
|
+
console.log("------------------------------------------------------------");
|
|
110
|
+
console.log();
|
|
111
|
+
console.log(" This sail adds push notification support to your app:");
|
|
112
|
+
console.log(" - Firebase Cloud Messaging (FCM) for delivery");
|
|
113
|
+
console.log(" - Capacitor integration for native iOS/Android");
|
|
114
|
+
console.log(" - Device token registration and management");
|
|
115
|
+
console.log(" - Server-side notification sending with firebase-admin");
|
|
116
|
+
console.log(" - React hook for permission handling and token lifecycle");
|
|
117
|
+
console.log();
|
|
118
|
+
|
|
119
|
+
const pkgPath = join(PROJECT_ROOT, "package.json");
|
|
120
|
+
if (existsSync(pkgPath)) {
|
|
121
|
+
const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
|
|
122
|
+
console.log(` Template version: ${pkg.version ?? "unknown"}`);
|
|
123
|
+
console.log();
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// -- Step 2: Firebase setup guide -------------------------------------------
|
|
127
|
+
const hasFirebase = await confirm({
|
|
128
|
+
message: "Do you already have a Firebase project with Cloud Messaging enabled?",
|
|
129
|
+
default: false,
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
if (!hasFirebase) {
|
|
133
|
+
console.log();
|
|
134
|
+
console.log(" Follow these steps to set up Firebase Cloud Messaging:");
|
|
135
|
+
console.log();
|
|
136
|
+
console.log(" 1. Go to https://console.firebase.google.com");
|
|
137
|
+
console.log(" 2. Click 'Add project' (or select an existing project)");
|
|
138
|
+
console.log(" 3. Follow the setup wizard (you can disable Google Analytics)");
|
|
139
|
+
console.log(" 4. In the project, go to Project Settings > Cloud Messaging");
|
|
140
|
+
console.log(" Make sure Cloud Messaging API (V1) is enabled");
|
|
141
|
+
console.log(" 5. Go to Project Settings > Service Accounts");
|
|
142
|
+
console.log(' 6. Click "Generate new private key" to download the JSON file');
|
|
143
|
+
console.log(" 7. Open the JSON file — you will need:");
|
|
144
|
+
console.log(" - project_id");
|
|
145
|
+
console.log(" - private_key (the full PEM string)");
|
|
146
|
+
console.log(" - client_email");
|
|
147
|
+
console.log();
|
|
148
|
+
|
|
149
|
+
await confirm({
|
|
150
|
+
message: "I have my Firebase project set up and service account JSON ready",
|
|
151
|
+
default: false,
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// -- Step 3: Collect credentials --------------------------------------------
|
|
156
|
+
console.log();
|
|
157
|
+
console.log(" Enter your Firebase service account credentials.");
|
|
158
|
+
console.log(" These are found in the service account JSON file you downloaded.");
|
|
159
|
+
console.log();
|
|
160
|
+
|
|
161
|
+
const firebaseProjectId = await input({
|
|
162
|
+
message: "Firebase Project ID (project_id):",
|
|
163
|
+
validate: (value) => {
|
|
164
|
+
if (!value || value.trim().length === 0) return "Project ID is required.";
|
|
165
|
+
return true;
|
|
166
|
+
},
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
const firebaseClientEmail = await input({
|
|
170
|
+
message: "Firebase Client Email (client_email):",
|
|
171
|
+
validate: (value) => {
|
|
172
|
+
if (!value || value.trim().length === 0) return "Client email is required.";
|
|
173
|
+
if (!value.includes("@")) return "Should be a valid service account email.";
|
|
174
|
+
return true;
|
|
175
|
+
},
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
console.log();
|
|
179
|
+
console.log(" For the private key, paste the full PEM key from the JSON file.");
|
|
180
|
+
console.log(" It starts with -----BEGIN PRIVATE KEY----- and ends with -----END PRIVATE KEY-----");
|
|
181
|
+
console.log(" You can paste it as a single line with \\n for newlines.");
|
|
182
|
+
console.log();
|
|
183
|
+
|
|
184
|
+
const firebasePrivateKey = await input({
|
|
185
|
+
message: "Firebase Private Key (private_key):",
|
|
186
|
+
validate: (value) => {
|
|
187
|
+
if (!value || value.trim().length === 0) return "Private key is required.";
|
|
188
|
+
if (!value.includes("PRIVATE KEY")) return "Should contain 'PRIVATE KEY' — paste the full PEM key.";
|
|
189
|
+
return true;
|
|
190
|
+
},
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
// -- Step 4: Summary --------------------------------------------------------
|
|
194
|
+
console.log();
|
|
195
|
+
console.log(" Summary of changes:");
|
|
196
|
+
console.log(" -------------------");
|
|
197
|
+
console.log(" Files to copy (backend):");
|
|
198
|
+
console.log(" + packages/backend/src/db/schema/notifications.ts");
|
|
199
|
+
console.log(" + packages/backend/src/routes/notifications.ts");
|
|
200
|
+
console.log(" + packages/backend/src/services/notifications.ts");
|
|
201
|
+
console.log();
|
|
202
|
+
console.log(" Files to copy (frontend):");
|
|
203
|
+
console.log(" + packages/frontend/src/hooks/usePushNotifications.ts");
|
|
204
|
+
console.log(" + packages/frontend/src/components/PushNotificationInit.tsx");
|
|
205
|
+
console.log();
|
|
206
|
+
console.log(" Files to modify:");
|
|
207
|
+
console.log(" ~ packages/backend/src/db/schema/index.ts");
|
|
208
|
+
console.log(" ~ packages/backend/src/index.ts");
|
|
209
|
+
console.log(" ~ packages/backend/src/env.ts");
|
|
210
|
+
console.log(" ~ packages/frontend/src/components/layout/Layout.tsx");
|
|
211
|
+
console.log(" ~ .env.example / .env");
|
|
212
|
+
console.log();
|
|
213
|
+
console.log(" Environment variables:");
|
|
214
|
+
console.log(` FIREBASE_PROJECT_ID=${firebaseProjectId}`);
|
|
215
|
+
console.log(` FIREBASE_CLIENT_EMAIL=${firebaseClientEmail.slice(0, 20)}...`);
|
|
216
|
+
console.log(` FIREBASE_PRIVATE_KEY=-----BEGIN PRIVATE KEY-----...`);
|
|
217
|
+
console.log();
|
|
218
|
+
|
|
219
|
+
// -- Step 5: Confirm --------------------------------------------------------
|
|
220
|
+
const proceed = await confirm({ message: "Proceed with installation?", default: true });
|
|
221
|
+
if (!proceed) { console.log("\n Installation cancelled.\n"); process.exit(0); }
|
|
222
|
+
|
|
223
|
+
console.log();
|
|
224
|
+
console.log(" Installing...");
|
|
225
|
+
console.log();
|
|
226
|
+
|
|
227
|
+
// -- Step 6: Copy files and insert at markers --------------------------------
|
|
228
|
+
console.log(" Copying backend files...");
|
|
229
|
+
const backendFiles = [
|
|
230
|
+
{ src: "backend/schema/notifications.ts", dest: "src/db/schema/notifications.ts" },
|
|
231
|
+
{ src: "backend/routes/notifications.ts", dest: "src/routes/notifications.ts" },
|
|
232
|
+
{ src: "backend/services/notifications.ts", dest: "src/services/notifications.ts" },
|
|
233
|
+
];
|
|
234
|
+
for (const f of backendFiles) {
|
|
235
|
+
copyFile(join(SAIL_DIR, "files", f.src), join(BACKEND_ROOT, f.dest), f.dest);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
console.log();
|
|
239
|
+
console.log(" Copying frontend files...");
|
|
240
|
+
const frontendFiles = [
|
|
241
|
+
{ src: "frontend/hooks/usePushNotifications.ts", dest: "src/hooks/usePushNotifications.ts" },
|
|
242
|
+
{ src: "frontend/components/PushNotificationInit.tsx", dest: "src/components/PushNotificationInit.tsx" },
|
|
243
|
+
];
|
|
244
|
+
for (const f of frontendFiles) {
|
|
245
|
+
copyFile(join(SAIL_DIR, "files", f.src), join(FRONTEND_ROOT, f.dest), f.dest);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
console.log();
|
|
249
|
+
console.log(" Modifying backend files...");
|
|
250
|
+
|
|
251
|
+
insertAtMarker(
|
|
252
|
+
join(BACKEND_ROOT, "src/db/schema/index.ts"),
|
|
253
|
+
"// [SAIL_SCHEMA]",
|
|
254
|
+
'export * from "./notifications.js";',
|
|
255
|
+
);
|
|
256
|
+
|
|
257
|
+
insertAtMarker(
|
|
258
|
+
join(BACKEND_ROOT, "src/index.ts"),
|
|
259
|
+
"// [SAIL_IMPORTS]",
|
|
260
|
+
'import { notificationsRouter } from "./routes/notifications.js";',
|
|
261
|
+
);
|
|
262
|
+
|
|
263
|
+
insertAtMarker(
|
|
264
|
+
join(BACKEND_ROOT, "src/index.ts"),
|
|
265
|
+
"// [SAIL_ROUTES]",
|
|
266
|
+
'app.use("/api/notifications", notificationsRouter);',
|
|
267
|
+
);
|
|
268
|
+
|
|
269
|
+
insertAtMarker(
|
|
270
|
+
join(BACKEND_ROOT, "src/env.ts"),
|
|
271
|
+
"// [SAIL_ENV_VARS]",
|
|
272
|
+
` FIREBASE_PROJECT_ID: z.string().min(1, "FIREBASE_PROJECT_ID is required"),\n FIREBASE_PRIVATE_KEY: z.string().min(1, "FIREBASE_PRIVATE_KEY is required"),\n FIREBASE_CLIENT_EMAIL: z.string().min(1, "FIREBASE_CLIENT_EMAIL is required"),`,
|
|
273
|
+
);
|
|
274
|
+
|
|
275
|
+
console.log();
|
|
276
|
+
console.log(" Modifying frontend files...");
|
|
277
|
+
|
|
278
|
+
// Add PushNotificationInit to Layout.tsx
|
|
279
|
+
const layoutPath = join(FRONTEND_ROOT, "src/components/layout/Layout.tsx");
|
|
280
|
+
if (existsSync(layoutPath)) {
|
|
281
|
+
let layoutContent = readFileSync(layoutPath, "utf-8");
|
|
282
|
+
|
|
283
|
+
// Add import if not present
|
|
284
|
+
const importLine = 'import { PushNotificationInit } from "@/components/PushNotificationInit.js";';
|
|
285
|
+
if (!layoutContent.includes("PushNotificationInit")) {
|
|
286
|
+
// Insert import after the last import statement
|
|
287
|
+
const lastImportIndex = layoutContent.lastIndexOf("import ");
|
|
288
|
+
const lineEnd = layoutContent.indexOf("\n", lastImportIndex);
|
|
289
|
+
layoutContent =
|
|
290
|
+
layoutContent.slice(0, lineEnd + 1) +
|
|
291
|
+
importLine + "\n" +
|
|
292
|
+
layoutContent.slice(lineEnd + 1);
|
|
293
|
+
|
|
294
|
+
// Insert <PushNotificationInit /> after useDeepLinks() call
|
|
295
|
+
layoutContent = layoutContent.replace(
|
|
296
|
+
"useDeepLinks();",
|
|
297
|
+
"useDeepLinks();\n\n return (\n",
|
|
298
|
+
);
|
|
299
|
+
|
|
300
|
+
// Actually, let's just insert the component in the JSX
|
|
301
|
+
// Revert the above — re-read the file cleanly
|
|
302
|
+
layoutContent = readFileSync(layoutPath, "utf-8");
|
|
303
|
+
|
|
304
|
+
// Add import at the top after last import
|
|
305
|
+
const lastImport = layoutContent.lastIndexOf("import ");
|
|
306
|
+
const importLineEnd = layoutContent.indexOf("\n", lastImport);
|
|
307
|
+
layoutContent =
|
|
308
|
+
layoutContent.slice(0, importLineEnd + 1) +
|
|
309
|
+
importLine + "\n" +
|
|
310
|
+
layoutContent.slice(importLineEnd + 1);
|
|
311
|
+
|
|
312
|
+
// Add component in JSX — insert right after the opening <div>
|
|
313
|
+
layoutContent = layoutContent.replace(
|
|
314
|
+
'<div className="flex min-h-screen flex-col bg-keel-navy">',
|
|
315
|
+
'<div className="flex min-h-screen flex-col bg-keel-navy">\n <PushNotificationInit />',
|
|
316
|
+
);
|
|
317
|
+
|
|
318
|
+
writeFileSync(layoutPath, layoutContent, "utf-8");
|
|
319
|
+
console.log(` Modified -> Layout.tsx`);
|
|
320
|
+
} else {
|
|
321
|
+
console.log(` Skipped (already present) -> Layout.tsx`);
|
|
322
|
+
}
|
|
323
|
+
} else {
|
|
324
|
+
console.warn(" Warning: Layout.tsx not found. Add <PushNotificationInit /> manually.");
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
console.log();
|
|
328
|
+
console.log(" Installing dependencies...");
|
|
329
|
+
installDeps(manifest.dependencies.backend, "packages/backend");
|
|
330
|
+
installDeps(manifest.dependencies.frontend, "packages/frontend");
|
|
331
|
+
|
|
332
|
+
console.log();
|
|
333
|
+
console.log(" Generating database migrations...");
|
|
334
|
+
try {
|
|
335
|
+
execSync("npx drizzle-kit generate", { cwd: BACKEND_ROOT, stdio: "inherit" });
|
|
336
|
+
} catch {
|
|
337
|
+
console.warn(" Warning: Could not generate migrations. Run manually: cd packages/backend && npx drizzle-kit generate");
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
console.log();
|
|
341
|
+
console.log(" Updating environment files...");
|
|
342
|
+
appendToEnvFiles("Push Notifications (Firebase)", {
|
|
343
|
+
FIREBASE_PROJECT_ID: firebaseProjectId,
|
|
344
|
+
FIREBASE_PRIVATE_KEY: firebasePrivateKey,
|
|
345
|
+
FIREBASE_CLIENT_EMAIL: firebaseClientEmail,
|
|
346
|
+
});
|
|
347
|
+
|
|
348
|
+
// -- Step 7: Next steps ------------------------------------------------------
|
|
349
|
+
console.log();
|
|
350
|
+
console.log("------------------------------------------------------------");
|
|
351
|
+
console.log(" Push Notifications installed successfully!");
|
|
352
|
+
console.log("------------------------------------------------------------");
|
|
353
|
+
console.log();
|
|
354
|
+
console.log(" Next steps:");
|
|
355
|
+
console.log();
|
|
356
|
+
console.log(" 1. Run database migrations:");
|
|
357
|
+
console.log(" npm run db:migrate");
|
|
358
|
+
console.log();
|
|
359
|
+
console.log(" 2. Configure iOS (APNs):");
|
|
360
|
+
console.log(" - Go to https://developer.apple.com/account/resources/authkeys/list");
|
|
361
|
+
console.log(' - Create an APNs authentication key (check "Apple Push Notifications service")');
|
|
362
|
+
console.log(" - Download the .p8 key file");
|
|
363
|
+
console.log(" - In Firebase Console > Project Settings > Cloud Messaging > iOS:");
|
|
364
|
+
console.log(" Upload the APNs key, enter Key ID and Team ID");
|
|
365
|
+
console.log();
|
|
366
|
+
console.log(" 3. Configure Android:");
|
|
367
|
+
console.log(" - In Firebase Console, add an Android app to your project");
|
|
368
|
+
console.log(" - Download google-services.json");
|
|
369
|
+
console.log(" - Place it in your Android project: android/app/google-services.json");
|
|
370
|
+
console.log();
|
|
371
|
+
console.log(" 4. Sync Capacitor:");
|
|
372
|
+
console.log(" npx cap sync");
|
|
373
|
+
console.log();
|
|
374
|
+
console.log(" 5. Start your dev server:");
|
|
375
|
+
console.log(" npm run dev");
|
|
376
|
+
console.log();
|
|
377
|
+
console.log(" Testing push notifications:");
|
|
378
|
+
console.log(" - Push notifications only work on physical devices (not simulators)");
|
|
379
|
+
console.log(" - Use the Firebase Console > Messaging to send test notifications");
|
|
380
|
+
console.log(" - Or use the POST /api/notifications/send endpoint from your backend");
|
|
381
|
+
console.log();
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
main().catch((err) => { console.error("Installation failed:", err); process.exit(1); });
|