@codaijs/keel 0.2.3 → 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,153 +1,153 @@
|
|
|
1
|
-
import { Router, type Request, type Response } from "express";
|
|
2
|
-
import { eq, and } from "drizzle-orm";
|
|
3
|
-
import { db } from "../db/index.js";
|
|
4
|
-
import { pushTokens } from "../db/schema/notifications.js";
|
|
5
|
-
import {
|
|
6
|
-
sendPushNotification,
|
|
7
|
-
sendMultiplePushNotifications,
|
|
8
|
-
} from "../services/notifications.js";
|
|
9
|
-
import { requireAuth } from "../middleware/auth.js";
|
|
10
|
-
|
|
11
|
-
export const notificationsRouter = Router();
|
|
12
|
-
|
|
13
|
-
// ---------------------------------------------------------------------------
|
|
14
|
-
// POST /register — Register a device push token
|
|
15
|
-
// ---------------------------------------------------------------------------
|
|
16
|
-
|
|
17
|
-
notificationsRouter.post(
|
|
18
|
-
"/register",
|
|
19
|
-
requireAuth,
|
|
20
|
-
async (req: Request, res: Response) => {
|
|
21
|
-
try {
|
|
22
|
-
const { token, platform } = req.body;
|
|
23
|
-
|
|
24
|
-
if (!token || typeof token !== "string") {
|
|
25
|
-
return res.status(400).json({ error: "token is required" });
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
const userId = req.user!.id;
|
|
29
|
-
|
|
30
|
-
// Check if this token is already registered for this user
|
|
31
|
-
const existing = await db.query.pushTokens.findFirst({
|
|
32
|
-
where: and(
|
|
33
|
-
eq(pushTokens.userId, userId),
|
|
34
|
-
eq(pushTokens.token, token),
|
|
35
|
-
),
|
|
36
|
-
});
|
|
37
|
-
|
|
38
|
-
if (existing) {
|
|
39
|
-
return res.json({ message: "Token already registered", id: existing.id });
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
// Insert the new token
|
|
43
|
-
const [inserted] = await db
|
|
44
|
-
.insert(pushTokens)
|
|
45
|
-
.values({
|
|
46
|
-
userId,
|
|
47
|
-
token,
|
|
48
|
-
platform: platform ?? null,
|
|
49
|
-
})
|
|
50
|
-
.returning({ id: pushTokens.id });
|
|
51
|
-
|
|
52
|
-
return res.status(201).json({ message: "Token registered", id: inserted.id });
|
|
53
|
-
} catch (error) {
|
|
54
|
-
console.error("Error registering push token:", error);
|
|
55
|
-
return res.status(500).json({ error: "Failed to register push token" });
|
|
56
|
-
}
|
|
57
|
-
},
|
|
58
|
-
);
|
|
59
|
-
|
|
60
|
-
// ---------------------------------------------------------------------------
|
|
61
|
-
// DELETE /unregister — Remove a device push token
|
|
62
|
-
// ---------------------------------------------------------------------------
|
|
63
|
-
|
|
64
|
-
notificationsRouter.delete(
|
|
65
|
-
"/unregister",
|
|
66
|
-
requireAuth,
|
|
67
|
-
async (req: Request, res: Response) => {
|
|
68
|
-
try {
|
|
69
|
-
const { token } = req.body;
|
|
70
|
-
|
|
71
|
-
if (!token || typeof token !== "string") {
|
|
72
|
-
return res.status(400).json({ error: "token is required" });
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
const userId = req.user!.id;
|
|
76
|
-
|
|
77
|
-
await db
|
|
78
|
-
.delete(pushTokens)
|
|
79
|
-
.where(
|
|
80
|
-
and(
|
|
81
|
-
eq(pushTokens.userId, userId),
|
|
82
|
-
eq(pushTokens.token, token),
|
|
83
|
-
),
|
|
84
|
-
);
|
|
85
|
-
|
|
86
|
-
return res.json({ message: "Token unregistered" });
|
|
87
|
-
} catch (error) {
|
|
88
|
-
console.error("Error unregistering push token:", error);
|
|
89
|
-
return res.status(500).json({ error: "Failed to unregister push token" });
|
|
90
|
-
}
|
|
91
|
-
},
|
|
92
|
-
);
|
|
93
|
-
|
|
94
|
-
// ---------------------------------------------------------------------------
|
|
95
|
-
// POST /send — Send a notification to a user (admin/internal use)
|
|
96
|
-
// ---------------------------------------------------------------------------
|
|
97
|
-
|
|
98
|
-
notificationsRouter.post(
|
|
99
|
-
"/send",
|
|
100
|
-
requireAuth,
|
|
101
|
-
async (req: Request, res: Response) => {
|
|
102
|
-
try {
|
|
103
|
-
const { userId, title, body, data } = req.body;
|
|
104
|
-
|
|
105
|
-
if (!userId || typeof userId !== "string") {
|
|
106
|
-
return res.status(400).json({ error: "userId is required" });
|
|
107
|
-
}
|
|
108
|
-
if (!title || typeof title !== "string") {
|
|
109
|
-
return res.status(400).json({ error: "title is required" });
|
|
110
|
-
}
|
|
111
|
-
if (!body || typeof body !== "string") {
|
|
112
|
-
return res.status(400).json({ error: "body is required" });
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
// Fetch all tokens for the target user
|
|
116
|
-
const tokens = await db.query.pushTokens.findMany({
|
|
117
|
-
where: eq(pushTokens.userId, userId),
|
|
118
|
-
});
|
|
119
|
-
|
|
120
|
-
if (tokens.length === 0) {
|
|
121
|
-
return res.status(404).json({ error: "No push tokens found for user" });
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
const tokenStrings = tokens.map((t) => t.token);
|
|
125
|
-
|
|
126
|
-
if (tokenStrings.length === 1) {
|
|
127
|
-
const messageId = await sendPushNotification(
|
|
128
|
-
tokenStrings[0],
|
|
129
|
-
title,
|
|
130
|
-
body,
|
|
131
|
-
data,
|
|
132
|
-
);
|
|
133
|
-
return res.json({ message: "Notification sent", messageId });
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
const result = await sendMultiplePushNotifications(
|
|
137
|
-
tokenStrings,
|
|
138
|
-
title,
|
|
139
|
-
body,
|
|
140
|
-
data,
|
|
141
|
-
);
|
|
142
|
-
|
|
143
|
-
return res.json({
|
|
144
|
-
message: "Notifications sent",
|
|
145
|
-
successCount: result.successCount,
|
|
146
|
-
failureCount: result.failureCount,
|
|
147
|
-
});
|
|
148
|
-
} catch (error) {
|
|
149
|
-
console.error("Error sending push notification:", error);
|
|
150
|
-
return res.status(500).json({ error: "Failed to send notification" });
|
|
151
|
-
}
|
|
152
|
-
},
|
|
153
|
-
);
|
|
1
|
+
import { Router, type Request, type Response } from "express";
|
|
2
|
+
import { eq, and } from "drizzle-orm";
|
|
3
|
+
import { db } from "../db/index.js";
|
|
4
|
+
import { pushTokens } from "../db/schema/notifications.js";
|
|
5
|
+
import {
|
|
6
|
+
sendPushNotification,
|
|
7
|
+
sendMultiplePushNotifications,
|
|
8
|
+
} from "../services/notifications.js";
|
|
9
|
+
import { requireAuth } from "../middleware/auth.js";
|
|
10
|
+
|
|
11
|
+
export const notificationsRouter = Router();
|
|
12
|
+
|
|
13
|
+
// ---------------------------------------------------------------------------
|
|
14
|
+
// POST /register — Register a device push token
|
|
15
|
+
// ---------------------------------------------------------------------------
|
|
16
|
+
|
|
17
|
+
notificationsRouter.post(
|
|
18
|
+
"/register",
|
|
19
|
+
requireAuth,
|
|
20
|
+
async (req: Request, res: Response) => {
|
|
21
|
+
try {
|
|
22
|
+
const { token, platform } = req.body;
|
|
23
|
+
|
|
24
|
+
if (!token || typeof token !== "string") {
|
|
25
|
+
return res.status(400).json({ error: "token is required" });
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const userId = req.user!.id;
|
|
29
|
+
|
|
30
|
+
// Check if this token is already registered for this user
|
|
31
|
+
const existing = await db.query.pushTokens.findFirst({
|
|
32
|
+
where: and(
|
|
33
|
+
eq(pushTokens.userId, userId),
|
|
34
|
+
eq(pushTokens.token, token),
|
|
35
|
+
),
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
if (existing) {
|
|
39
|
+
return res.json({ message: "Token already registered", id: existing.id });
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Insert the new token
|
|
43
|
+
const [inserted] = await db
|
|
44
|
+
.insert(pushTokens)
|
|
45
|
+
.values({
|
|
46
|
+
userId,
|
|
47
|
+
token,
|
|
48
|
+
platform: platform ?? null,
|
|
49
|
+
})
|
|
50
|
+
.returning({ id: pushTokens.id });
|
|
51
|
+
|
|
52
|
+
return res.status(201).json({ message: "Token registered", id: inserted.id });
|
|
53
|
+
} catch (error) {
|
|
54
|
+
console.error("Error registering push token:", error);
|
|
55
|
+
return res.status(500).json({ error: "Failed to register push token" });
|
|
56
|
+
}
|
|
57
|
+
},
|
|
58
|
+
);
|
|
59
|
+
|
|
60
|
+
// ---------------------------------------------------------------------------
|
|
61
|
+
// DELETE /unregister — Remove a device push token
|
|
62
|
+
// ---------------------------------------------------------------------------
|
|
63
|
+
|
|
64
|
+
notificationsRouter.delete(
|
|
65
|
+
"/unregister",
|
|
66
|
+
requireAuth,
|
|
67
|
+
async (req: Request, res: Response) => {
|
|
68
|
+
try {
|
|
69
|
+
const { token } = req.body;
|
|
70
|
+
|
|
71
|
+
if (!token || typeof token !== "string") {
|
|
72
|
+
return res.status(400).json({ error: "token is required" });
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const userId = req.user!.id;
|
|
76
|
+
|
|
77
|
+
await db
|
|
78
|
+
.delete(pushTokens)
|
|
79
|
+
.where(
|
|
80
|
+
and(
|
|
81
|
+
eq(pushTokens.userId, userId),
|
|
82
|
+
eq(pushTokens.token, token),
|
|
83
|
+
),
|
|
84
|
+
);
|
|
85
|
+
|
|
86
|
+
return res.json({ message: "Token unregistered" });
|
|
87
|
+
} catch (error) {
|
|
88
|
+
console.error("Error unregistering push token:", error);
|
|
89
|
+
return res.status(500).json({ error: "Failed to unregister push token" });
|
|
90
|
+
}
|
|
91
|
+
},
|
|
92
|
+
);
|
|
93
|
+
|
|
94
|
+
// ---------------------------------------------------------------------------
|
|
95
|
+
// POST /send — Send a notification to a user (admin/internal use)
|
|
96
|
+
// ---------------------------------------------------------------------------
|
|
97
|
+
|
|
98
|
+
notificationsRouter.post(
|
|
99
|
+
"/send",
|
|
100
|
+
requireAuth,
|
|
101
|
+
async (req: Request, res: Response) => {
|
|
102
|
+
try {
|
|
103
|
+
const { userId, title, body, data } = req.body;
|
|
104
|
+
|
|
105
|
+
if (!userId || typeof userId !== "string") {
|
|
106
|
+
return res.status(400).json({ error: "userId is required" });
|
|
107
|
+
}
|
|
108
|
+
if (!title || typeof title !== "string") {
|
|
109
|
+
return res.status(400).json({ error: "title is required" });
|
|
110
|
+
}
|
|
111
|
+
if (!body || typeof body !== "string") {
|
|
112
|
+
return res.status(400).json({ error: "body is required" });
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Fetch all tokens for the target user
|
|
116
|
+
const tokens = await db.query.pushTokens.findMany({
|
|
117
|
+
where: eq(pushTokens.userId, userId),
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
if (tokens.length === 0) {
|
|
121
|
+
return res.status(404).json({ error: "No push tokens found for user" });
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const tokenStrings = tokens.map((t) => t.token);
|
|
125
|
+
|
|
126
|
+
if (tokenStrings.length === 1) {
|
|
127
|
+
const messageId = await sendPushNotification(
|
|
128
|
+
tokenStrings[0],
|
|
129
|
+
title,
|
|
130
|
+
body,
|
|
131
|
+
data,
|
|
132
|
+
);
|
|
133
|
+
return res.json({ message: "Notification sent", messageId });
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const result = await sendMultiplePushNotifications(
|
|
137
|
+
tokenStrings,
|
|
138
|
+
title,
|
|
139
|
+
body,
|
|
140
|
+
data,
|
|
141
|
+
);
|
|
142
|
+
|
|
143
|
+
return res.json({
|
|
144
|
+
message: "Notifications sent",
|
|
145
|
+
successCount: result.successCount,
|
|
146
|
+
failureCount: result.failureCount,
|
|
147
|
+
});
|
|
148
|
+
} catch (error) {
|
|
149
|
+
console.error("Error sending push notification:", error);
|
|
150
|
+
return res.status(500).json({ error: "Failed to send notification" });
|
|
151
|
+
}
|
|
152
|
+
},
|
|
153
|
+
);
|
|
@@ -1,31 +1,31 @@
|
|
|
1
|
-
import { pgTable, text, varchar, timestamp } from "drizzle-orm/pg-core";
|
|
2
|
-
import { relations } from "drizzle-orm";
|
|
3
|
-
import { users } from "./users.js";
|
|
4
|
-
|
|
5
|
-
/**
|
|
6
|
-
* Push notification device tokens table.
|
|
7
|
-
*
|
|
8
|
-
* Stores FCM device tokens for each user. A user can have multiple tokens
|
|
9
|
-
* (one per device). Tokens are registered when the user grants push permission
|
|
10
|
-
* on a native device and removed on logout or token invalidation.
|
|
11
|
-
*/
|
|
12
|
-
export const pushTokens = pgTable("push_tokens", {
|
|
13
|
-
id: text("id")
|
|
14
|
-
.primaryKey()
|
|
15
|
-
.$defaultFn(() => crypto.randomUUID()),
|
|
16
|
-
userId: text("user_id")
|
|
17
|
-
.notNull()
|
|
18
|
-
.references(() => users.id, { onDelete: "cascade" }),
|
|
19
|
-
token: text("token").notNull(),
|
|
20
|
-
platform: varchar("platform", { length: 20 }),
|
|
21
|
-
createdAt: timestamp("created_at", { withTimezone: true })
|
|
22
|
-
.notNull()
|
|
23
|
-
.defaultNow(),
|
|
24
|
-
});
|
|
25
|
-
|
|
26
|
-
export const pushTokensRelations = relations(pushTokens, ({ one }) => ({
|
|
27
|
-
user: one(users, {
|
|
28
|
-
fields: [pushTokens.userId],
|
|
29
|
-
references: [users.id],
|
|
30
|
-
}),
|
|
31
|
-
}));
|
|
1
|
+
import { pgTable, text, varchar, timestamp } from "drizzle-orm/pg-core";
|
|
2
|
+
import { relations } from "drizzle-orm";
|
|
3
|
+
import { users } from "./users.js";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Push notification device tokens table.
|
|
7
|
+
*
|
|
8
|
+
* Stores FCM device tokens for each user. A user can have multiple tokens
|
|
9
|
+
* (one per device). Tokens are registered when the user grants push permission
|
|
10
|
+
* on a native device and removed on logout or token invalidation.
|
|
11
|
+
*/
|
|
12
|
+
export const pushTokens = pgTable("push_tokens", {
|
|
13
|
+
id: text("id")
|
|
14
|
+
.primaryKey()
|
|
15
|
+
.$defaultFn(() => crypto.randomUUID()),
|
|
16
|
+
userId: text("user_id")
|
|
17
|
+
.notNull()
|
|
18
|
+
.references(() => users.id, { onDelete: "cascade" }),
|
|
19
|
+
token: text("token").notNull(),
|
|
20
|
+
platform: varchar("platform", { length: 20 }),
|
|
21
|
+
createdAt: timestamp("created_at", { withTimezone: true })
|
|
22
|
+
.notNull()
|
|
23
|
+
.defaultNow(),
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
export const pushTokensRelations = relations(pushTokens, ({ one }) => ({
|
|
27
|
+
user: one(users, {
|
|
28
|
+
fields: [pushTokens.userId],
|
|
29
|
+
references: [users.id],
|
|
30
|
+
}),
|
|
31
|
+
}));
|
|
@@ -1,117 +1,117 @@
|
|
|
1
|
-
import admin from "firebase-admin";
|
|
2
|
-
import { env } from "../env.js";
|
|
3
|
-
|
|
4
|
-
// ---------------------------------------------------------------------------
|
|
5
|
-
// Firebase Admin initialization
|
|
6
|
-
// ---------------------------------------------------------------------------
|
|
7
|
-
|
|
8
|
-
if (!admin.apps.length) {
|
|
9
|
-
admin.initializeApp({
|
|
10
|
-
credential: admin.credential.cert({
|
|
11
|
-
projectId: env.FIREBASE_PROJECT_ID,
|
|
12
|
-
privateKey: env.FIREBASE_PRIVATE_KEY.replace(/\\n/g, "\n"),
|
|
13
|
-
clientEmail: env.FIREBASE_CLIENT_EMAIL,
|
|
14
|
-
}),
|
|
15
|
-
});
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
const messaging = admin.messaging();
|
|
19
|
-
|
|
20
|
-
// ---------------------------------------------------------------------------
|
|
21
|
-
// Send a push notification to a single device
|
|
22
|
-
// ---------------------------------------------------------------------------
|
|
23
|
-
|
|
24
|
-
/**
|
|
25
|
-
* Send a push notification to a single device token.
|
|
26
|
-
*
|
|
27
|
-
* @param token - The FCM device token
|
|
28
|
-
* @param title - Notification title
|
|
29
|
-
* @param body - Notification body text
|
|
30
|
-
* @param data - Optional key-value data payload (for deep linking, etc.)
|
|
31
|
-
* @returns The message ID from Firebase
|
|
32
|
-
*/
|
|
33
|
-
export async function sendPushNotification(
|
|
34
|
-
token: string,
|
|
35
|
-
title: string,
|
|
36
|
-
body: string,
|
|
37
|
-
data?: Record<string, string>,
|
|
38
|
-
): Promise<string> {
|
|
39
|
-
const message: admin.messaging.Message = {
|
|
40
|
-
token,
|
|
41
|
-
notification: {
|
|
42
|
-
title,
|
|
43
|
-
body,
|
|
44
|
-
},
|
|
45
|
-
data,
|
|
46
|
-
android: {
|
|
47
|
-
priority: "high",
|
|
48
|
-
notification: {
|
|
49
|
-
sound: "default",
|
|
50
|
-
channelId: "default",
|
|
51
|
-
},
|
|
52
|
-
},
|
|
53
|
-
apns: {
|
|
54
|
-
payload: {
|
|
55
|
-
aps: {
|
|
56
|
-
sound: "default",
|
|
57
|
-
badge: 1,
|
|
58
|
-
},
|
|
59
|
-
},
|
|
60
|
-
},
|
|
61
|
-
};
|
|
62
|
-
|
|
63
|
-
return messaging.send(message);
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
// ---------------------------------------------------------------------------
|
|
67
|
-
// Send push notifications to multiple devices
|
|
68
|
-
// ---------------------------------------------------------------------------
|
|
69
|
-
|
|
70
|
-
/**
|
|
71
|
-
* Send a push notification to multiple device tokens.
|
|
72
|
-
*
|
|
73
|
-
* Uses `sendEachForMulticast` to handle per-token delivery. Returns the
|
|
74
|
-
* batch response so callers can check individual success/failure.
|
|
75
|
-
*
|
|
76
|
-
* @param tokens - Array of FCM device tokens
|
|
77
|
-
* @param title - Notification title
|
|
78
|
-
* @param body - Notification body text
|
|
79
|
-
* @param data - Optional key-value data payload
|
|
80
|
-
* @returns The batch response from Firebase
|
|
81
|
-
*/
|
|
82
|
-
export async function sendMultiplePushNotifications(
|
|
83
|
-
tokens: string[],
|
|
84
|
-
title: string,
|
|
85
|
-
body: string,
|
|
86
|
-
data?: Record<string, string>,
|
|
87
|
-
): Promise<admin.messaging.BatchResponse> {
|
|
88
|
-
if (tokens.length === 0) {
|
|
89
|
-
return { responses: [], successCount: 0, failureCount: 0 };
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
const message: admin.messaging.MulticastMessage = {
|
|
93
|
-
tokens,
|
|
94
|
-
notification: {
|
|
95
|
-
title,
|
|
96
|
-
body,
|
|
97
|
-
},
|
|
98
|
-
data,
|
|
99
|
-
android: {
|
|
100
|
-
priority: "high",
|
|
101
|
-
notification: {
|
|
102
|
-
sound: "default",
|
|
103
|
-
channelId: "default",
|
|
104
|
-
},
|
|
105
|
-
},
|
|
106
|
-
apns: {
|
|
107
|
-
payload: {
|
|
108
|
-
aps: {
|
|
109
|
-
sound: "default",
|
|
110
|
-
badge: 1,
|
|
111
|
-
},
|
|
112
|
-
},
|
|
113
|
-
},
|
|
114
|
-
};
|
|
115
|
-
|
|
116
|
-
return messaging.sendEachForMulticast(message);
|
|
117
|
-
}
|
|
1
|
+
import admin from "firebase-admin";
|
|
2
|
+
import { env } from "../env.js";
|
|
3
|
+
|
|
4
|
+
// ---------------------------------------------------------------------------
|
|
5
|
+
// Firebase Admin initialization
|
|
6
|
+
// ---------------------------------------------------------------------------
|
|
7
|
+
|
|
8
|
+
if (!admin.apps.length) {
|
|
9
|
+
admin.initializeApp({
|
|
10
|
+
credential: admin.credential.cert({
|
|
11
|
+
projectId: env.FIREBASE_PROJECT_ID,
|
|
12
|
+
privateKey: env.FIREBASE_PRIVATE_KEY.replace(/\\n/g, "\n"),
|
|
13
|
+
clientEmail: env.FIREBASE_CLIENT_EMAIL,
|
|
14
|
+
}),
|
|
15
|
+
});
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const messaging = admin.messaging();
|
|
19
|
+
|
|
20
|
+
// ---------------------------------------------------------------------------
|
|
21
|
+
// Send a push notification to a single device
|
|
22
|
+
// ---------------------------------------------------------------------------
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Send a push notification to a single device token.
|
|
26
|
+
*
|
|
27
|
+
* @param token - The FCM device token
|
|
28
|
+
* @param title - Notification title
|
|
29
|
+
* @param body - Notification body text
|
|
30
|
+
* @param data - Optional key-value data payload (for deep linking, etc.)
|
|
31
|
+
* @returns The message ID from Firebase
|
|
32
|
+
*/
|
|
33
|
+
export async function sendPushNotification(
|
|
34
|
+
token: string,
|
|
35
|
+
title: string,
|
|
36
|
+
body: string,
|
|
37
|
+
data?: Record<string, string>,
|
|
38
|
+
): Promise<string> {
|
|
39
|
+
const message: admin.messaging.Message = {
|
|
40
|
+
token,
|
|
41
|
+
notification: {
|
|
42
|
+
title,
|
|
43
|
+
body,
|
|
44
|
+
},
|
|
45
|
+
data,
|
|
46
|
+
android: {
|
|
47
|
+
priority: "high",
|
|
48
|
+
notification: {
|
|
49
|
+
sound: "default",
|
|
50
|
+
channelId: "default",
|
|
51
|
+
},
|
|
52
|
+
},
|
|
53
|
+
apns: {
|
|
54
|
+
payload: {
|
|
55
|
+
aps: {
|
|
56
|
+
sound: "default",
|
|
57
|
+
badge: 1,
|
|
58
|
+
},
|
|
59
|
+
},
|
|
60
|
+
},
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
return messaging.send(message);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// ---------------------------------------------------------------------------
|
|
67
|
+
// Send push notifications to multiple devices
|
|
68
|
+
// ---------------------------------------------------------------------------
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Send a push notification to multiple device tokens.
|
|
72
|
+
*
|
|
73
|
+
* Uses `sendEachForMulticast` to handle per-token delivery. Returns the
|
|
74
|
+
* batch response so callers can check individual success/failure.
|
|
75
|
+
*
|
|
76
|
+
* @param tokens - Array of FCM device tokens
|
|
77
|
+
* @param title - Notification title
|
|
78
|
+
* @param body - Notification body text
|
|
79
|
+
* @param data - Optional key-value data payload
|
|
80
|
+
* @returns The batch response from Firebase
|
|
81
|
+
*/
|
|
82
|
+
export async function sendMultiplePushNotifications(
|
|
83
|
+
tokens: string[],
|
|
84
|
+
title: string,
|
|
85
|
+
body: string,
|
|
86
|
+
data?: Record<string, string>,
|
|
87
|
+
): Promise<admin.messaging.BatchResponse> {
|
|
88
|
+
if (tokens.length === 0) {
|
|
89
|
+
return { responses: [], successCount: 0, failureCount: 0 };
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const message: admin.messaging.MulticastMessage = {
|
|
93
|
+
tokens,
|
|
94
|
+
notification: {
|
|
95
|
+
title,
|
|
96
|
+
body,
|
|
97
|
+
},
|
|
98
|
+
data,
|
|
99
|
+
android: {
|
|
100
|
+
priority: "high",
|
|
101
|
+
notification: {
|
|
102
|
+
sound: "default",
|
|
103
|
+
channelId: "default",
|
|
104
|
+
},
|
|
105
|
+
},
|
|
106
|
+
apns: {
|
|
107
|
+
payload: {
|
|
108
|
+
aps: {
|
|
109
|
+
sound: "default",
|
|
110
|
+
badge: 1,
|
|
111
|
+
},
|
|
112
|
+
},
|
|
113
|
+
},
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
return messaging.sendEachForMulticast(message);
|
|
117
|
+
}
|
|
@@ -1,12 +1,12 @@
|
|
|
1
|
-
import { usePushNotifications } from "@/hooks/usePushNotifications.js";
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Invisible component that initializes push notification registration.
|
|
5
|
-
*
|
|
6
|
-
* Place this inside Layout.tsx so that push notifications are set up as soon
|
|
7
|
-
* as the app mounts on a native device. Renders nothing visible.
|
|
8
|
-
*/
|
|
9
|
-
export function PushNotificationInit() {
|
|
10
|
-
usePushNotifications();
|
|
11
|
-
return null;
|
|
12
|
-
}
|
|
1
|
+
import { usePushNotifications } from "@/hooks/usePushNotifications.js";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Invisible component that initializes push notification registration.
|
|
5
|
+
*
|
|
6
|
+
* Place this inside Layout.tsx so that push notifications are set up as soon
|
|
7
|
+
* as the app mounts on a native device. Renders nothing visible.
|
|
8
|
+
*/
|
|
9
|
+
export function PushNotificationInit() {
|
|
10
|
+
usePushNotifications();
|
|
11
|
+
return null;
|
|
12
|
+
}
|