@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.
Files changed (80) hide show
  1. package/dist/__tests__/sail-installer.test.js +25 -25
  2. package/dist/sail-installer.js +174 -174
  3. package/dist/scaffold.js +68 -68
  4. package/package.json +58 -58
  5. package/sails/_template/addon.json +20 -20
  6. package/sails/_template/install.ts +402 -402
  7. package/sails/admin-dashboard/README.md +117 -117
  8. package/sails/admin-dashboard/addon.json +28 -28
  9. package/sails/admin-dashboard/files/backend/middleware/admin.ts +34 -34
  10. package/sails/admin-dashboard/files/backend/routes/admin.ts +243 -243
  11. package/sails/admin-dashboard/files/frontend/components/admin/StatsCard.tsx +40 -40
  12. package/sails/admin-dashboard/files/frontend/components/admin/UsersTable.tsx +240 -240
  13. package/sails/admin-dashboard/files/frontend/hooks/useAdmin.ts +149 -149
  14. package/sails/admin-dashboard/files/frontend/pages/admin/Dashboard.tsx +173 -173
  15. package/sails/admin-dashboard/files/frontend/pages/admin/UserDetail.tsx +203 -203
  16. package/sails/admin-dashboard/install.ts +305 -305
  17. package/sails/analytics/README.md +178 -178
  18. package/sails/analytics/addon.json +27 -27
  19. package/sails/analytics/files/frontend/components/AnalyticsProvider.tsx +58 -58
  20. package/sails/analytics/files/frontend/hooks/useAnalytics.ts +64 -64
  21. package/sails/analytics/files/frontend/lib/analytics.ts +103 -103
  22. package/sails/analytics/install.ts +297 -297
  23. package/sails/file-uploads/addon.json +30 -30
  24. package/sails/file-uploads/files/backend/routes/files.ts +198 -198
  25. package/sails/file-uploads/files/backend/schema/files.ts +36 -36
  26. package/sails/file-uploads/files/backend/services/file-storage.ts +128 -128
  27. package/sails/file-uploads/files/frontend/components/FileList.tsx +248 -248
  28. package/sails/file-uploads/files/frontend/components/FileUploadButton.tsx +147 -147
  29. package/sails/file-uploads/files/frontend/hooks/useFileUpload.ts +106 -106
  30. package/sails/file-uploads/files/frontend/hooks/useFiles.ts +118 -118
  31. package/sails/file-uploads/files/frontend/pages/Files.tsx +37 -37
  32. package/sails/file-uploads/install.ts +466 -466
  33. package/sails/gdpr/README.md +174 -174
  34. package/sails/gdpr/addon.json +27 -27
  35. package/sails/gdpr/files/backend/routes/gdpr.ts +140 -140
  36. package/sails/gdpr/files/backend/services/gdpr.ts +293 -293
  37. package/sails/gdpr/files/frontend/components/auth/ConsentCheckboxes.tsx +97 -97
  38. package/sails/gdpr/files/frontend/components/gdpr/AccountDeletionRequest.tsx +192 -192
  39. package/sails/gdpr/files/frontend/components/gdpr/DataExportButton.tsx +75 -75
  40. package/sails/gdpr/files/frontend/pages/PrivacyPolicy.tsx +186 -186
  41. package/sails/gdpr/install.ts +756 -756
  42. package/sails/google-oauth/README.md +121 -121
  43. package/sails/google-oauth/addon.json +22 -22
  44. package/sails/google-oauth/files/GoogleButton.tsx +50 -50
  45. package/sails/google-oauth/install.ts +252 -252
  46. package/sails/i18n/README.md +193 -193
  47. package/sails/i18n/addon.json +30 -30
  48. package/sails/i18n/files/frontend/components/LanguageSwitcher.tsx +108 -108
  49. package/sails/i18n/files/frontend/hooks/useLanguage.ts +31 -31
  50. package/sails/i18n/files/frontend/lib/i18n.ts +32 -32
  51. package/sails/i18n/files/frontend/locales/de/common.json +44 -44
  52. package/sails/i18n/files/frontend/locales/en/common.json +44 -44
  53. package/sails/i18n/install.ts +407 -407
  54. package/sails/push-notifications/README.md +163 -163
  55. package/sails/push-notifications/addon.json +31 -31
  56. package/sails/push-notifications/files/backend/routes/notifications.ts +153 -153
  57. package/sails/push-notifications/files/backend/schema/notifications.ts +31 -31
  58. package/sails/push-notifications/files/backend/services/notifications.ts +117 -117
  59. package/sails/push-notifications/files/frontend/components/PushNotificationInit.tsx +12 -12
  60. package/sails/push-notifications/files/frontend/hooks/usePushNotifications.ts +154 -154
  61. package/sails/push-notifications/install.ts +384 -384
  62. package/sails/r2-storage/addon.json +29 -29
  63. package/sails/r2-storage/files/backend/services/storage.ts +71 -71
  64. package/sails/r2-storage/files/frontend/components/ProfilePictureUpload.tsx +167 -167
  65. package/sails/r2-storage/install.ts +412 -412
  66. package/sails/rate-limiting/addon.json +20 -20
  67. package/sails/rate-limiting/files/backend/middleware/rate-limit-store.ts +104 -104
  68. package/sails/rate-limiting/files/backend/middleware/rate-limit.ts +137 -137
  69. package/sails/rate-limiting/install.ts +300 -300
  70. package/sails/registry.json +107 -107
  71. package/sails/stripe/README.md +214 -214
  72. package/sails/stripe/addon.json +24 -24
  73. package/sails/stripe/files/backend/routes/stripe.ts +154 -154
  74. package/sails/stripe/files/backend/schema/stripe.ts +74 -74
  75. package/sails/stripe/files/backend/services/stripe.ts +224 -224
  76. package/sails/stripe/files/frontend/components/SubscriptionStatus.tsx +135 -135
  77. package/sails/stripe/files/frontend/hooks/useSubscription.ts +86 -86
  78. package/sails/stripe/files/frontend/pages/Checkout.tsx +116 -116
  79. package/sails/stripe/files/frontend/pages/Pricing.tsx +226 -226
  80. 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
+ }