@flink-app/firebase-messaging-plugin 0.12.1-alpha.9 → 0.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,4 +1,4 @@
1
- // Generated Fri Apr 11 2025 20:43:12 GMT+0200 (Central European Summer Time)
1
+ // Generated Fri Jan 09 2026 11:18:24 GMT+0100 (Central European Standard Time)
2
2
  import { autoRegisteredHandlers, HttpMethod } from "@flink-app/flink";
3
3
  import * as PostMessage_0 from "../src/handlers/PostMessage";
4
4
 
@@ -1,4 +1,4 @@
1
- // Generated Fri Apr 11 2025 20:43:12 GMT+0200 (Central European Summer Time)
1
+ // Generated Fri Jan 09 2026 11:18:25 GMT+0100 (Central European Standard Time)
2
2
  import { autoRegisteredJobs } from "@flink-app/flink";
3
3
  export const jobs = [];
4
4
  autoRegisteredJobs.push(...jobs);
@@ -1,4 +1,4 @@
1
- // Generated Fri Apr 11 2025 20:43:12 GMT+0200 (Central European Summer Time)
1
+ // Generated Fri Jan 09 2026 11:18:24 GMT+0100 (Central European Standard Time)
2
2
  import { autoRegisteredRepos } from "@flink-app/flink";
3
3
  export const repos = [];
4
4
  autoRegisteredRepos.push(...repos);
@@ -1,7 +1,7 @@
1
1
  import Message from "../../src/schemas/Message";
2
2
  import SendResult from "../../src/schemas/SendResult";
3
3
 
4
- // Generated Fri Apr 11 2025 20:43:12 GMT+0200 (Central European Summer Time)
4
+ // Generated Fri Jan 09 2026 11:18:25 GMT+0100 (Central European Standard Time)
5
5
  export interface PostMessage_12_ReqSchema extends Message {}
6
6
 
7
7
  export interface PostMessage_12_ResSchema extends SendResult {}
package/.flink/start.ts CHANGED
@@ -1,5 +1,6 @@
1
- // Generated Fri Apr 11 2025 20:43:12 GMT+0200 (Central European Summer Time)
1
+ // Generated Fri Jan 09 2026 11:18:25 GMT+0100 (Central European Standard Time)
2
2
  import "./generatedHandlers";
3
3
  import "./generatedRepos";
4
4
  import "./generatedJobs";
5
5
  import "../src/index";
6
+ export default {}; // Export an empty object to make it a module
package/CHANGELOG.md ADDED
@@ -0,0 +1,12 @@
1
+ # @flink-app/firebase-messaging-plugin
2
+
3
+ ## 0.13.0
4
+
5
+ ### Minor Changes
6
+
7
+ - Migrate to pnpm and streamlines build process.
8
+
9
+ ### Patch Changes
10
+
11
+ - Updated dependencies
12
+ - @flink-app/management-api-plugin@0.13.0
@@ -24,7 +24,7 @@ var __importStar = (this && this.__importStar) || function (mod) {
24
24
  };
25
25
  Object.defineProperty(exports, "__esModule", { value: true });
26
26
  exports.handlers = void 0;
27
- // Generated Fri Apr 11 2025 20:43:12 GMT+0200 (Central European Summer Time)
27
+ // Generated Fri Jan 09 2026 11:18:24 GMT+0100 (Central European Standard Time)
28
28
  var flink_1 = require("@flink-app/flink");
29
29
  var PostMessage_0 = __importStar(require("../src/handlers/PostMessage"));
30
30
  exports.handlers = [{ handler: PostMessage_0, assumedHttpMethod: flink_1.HttpMethod.post }];
@@ -1,7 +1,7 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.jobs = void 0;
4
- // Generated Fri Apr 11 2025 20:43:12 GMT+0200 (Central European Summer Time)
4
+ // Generated Fri Jan 09 2026 11:18:25 GMT+0100 (Central European Standard Time)
5
5
  var flink_1 = require("@flink-app/flink");
6
6
  exports.jobs = [];
7
7
  flink_1.autoRegisteredJobs.push.apply(flink_1.autoRegisteredJobs, exports.jobs);
@@ -1,7 +1,7 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.repos = void 0;
4
- // Generated Fri Apr 11 2025 20:43:12 GMT+0200 (Central European Summer Time)
4
+ // Generated Fri Jan 09 2026 11:18:24 GMT+0100 (Central European Standard Time)
5
5
  var flink_1 = require("@flink-app/flink");
6
6
  exports.repos = [];
7
7
  flink_1.autoRegisteredRepos.push.apply(flink_1.autoRegisteredRepos, exports.repos);
@@ -2,3 +2,5 @@ import "./generatedHandlers";
2
2
  import "./generatedRepos";
3
3
  import "./generatedJobs";
4
4
  import "../src/index";
5
+ declare const _default: {};
6
+ export default _default;
@@ -1,7 +1,8 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- // Generated Fri Apr 11 2025 20:43:12 GMT+0200 (Central European Summer Time)
3
+ // Generated Fri Jan 09 2026 11:18:25 GMT+0100 (Central European Standard Time)
4
4
  require("./generatedHandlers");
5
5
  require("./generatedRepos");
6
6
  require("./generatedJobs");
7
7
  require("../src/index");
8
+ exports.default = {}; // Export an empty object to make it a module
@@ -43,8 +43,8 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
43
43
  });
44
44
  };
45
45
  var __generator = (this && this.__generator) || function (thisArg, body) {
46
- var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g;
47
- return g = { next: verb(0), "throw": verb(1), "return": verb(2) }, typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g;
46
+ var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype);
47
+ return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g;
48
48
  function verb(n) { return function (v) { return step([n, v]); }; }
49
49
  function step(op) {
50
50
  if (f) throw new TypeError("Generator is already executing.");
@@ -9,8 +9,8 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
9
9
  });
10
10
  };
11
11
  var __generator = (this && this.__generator) || function (thisArg, body) {
12
- var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g;
13
- return g = { next: verb(0), "throw": verb(1), "return": verb(2) }, typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g;
12
+ var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype);
13
+ return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g;
14
14
  function verb(n) { return function (v) { return step([n, v]); }; }
15
15
  function step(op) {
16
16
  if (f) throw new TypeError("Generator is already executing.");
@@ -9,8 +9,8 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
9
9
  });
10
10
  };
11
11
  var __generator = (this && this.__generator) || function (thisArg, body) {
12
- var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g;
13
- return g = { next: verb(0), "throw": verb(1), "return": verb(2) }, typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g;
12
+ var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype);
13
+ return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g;
14
14
  function verb(n) { return function (v) { return step([n, v]); }; }
15
15
  function step(op) {
16
16
  if (f) throw new TypeError("Generator is already executing.");
@@ -9,8 +9,8 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
9
9
  });
10
10
  };
11
11
  var __generator = (this && this.__generator) || function (thisArg, body) {
12
- var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g;
13
- return g = { next: verb(0), "throw": verb(1), "return": verb(2) }, typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g;
12
+ var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype);
13
+ return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g;
14
14
  function verb(n) { return function (v) { return step([n, v]); }; }
15
15
  function step(op) {
16
16
  if (f) throw new TypeError("Generator is already executing.");
package/package.json CHANGED
@@ -1,32 +1,27 @@
1
1
  {
2
- "name": "@flink-app/firebase-messaging-plugin",
3
- "version": "0.12.1-alpha.9",
4
- "description": "Flink plugin to send Firebase cloud messages",
5
- "scripts": {
6
- "test": "echo \"Error: no test specified\"",
7
- "build": "flink build",
8
- "prepare": "npm run build",
9
- "watch": "nodemon --exec \"flink build\""
10
- },
11
- "author": "joel@frost.se",
12
- "license": "MIT",
13
- "types": "dist/src/index.d.ts",
14
- "main": "dist/src/index.js",
15
- "publishConfig": {
16
- "access": "public"
17
- },
18
- "dependencies": {
19
- "@flink-app/management-api-plugin": "^0.12.1-alpha.9",
20
- "firebase-admin": "^12.1.1"
21
- },
22
- "devDependencies": {
23
- "@flink-app/flink": "^0.12.1-alpha.9",
24
- "@types/express": "^4.17.12",
25
- "@types/node": "22.13.10",
26
- "nodemon": "^2.0.7",
27
- "ts-node": "^9.1.1",
28
- "tsc-watch": "^4.2.9",
29
- "typescript": "5.4.5"
30
- },
31
- "gitHead": "3007ad036607014176930446fde56203bde8d6f5"
32
- }
2
+ "name": "@flink-app/firebase-messaging-plugin",
3
+ "version": "0.13.0",
4
+ "description": "Flink plugin to send Firebase cloud messages",
5
+ "author": "joel@frost.se",
6
+ "license": "MIT",
7
+ "types": "dist/src/index.d.ts",
8
+ "main": "dist/src/index.js",
9
+ "publishConfig": {
10
+ "access": "public"
11
+ },
12
+ "dependencies": {
13
+ "firebase-admin": "^12.1.1",
14
+ "@flink-app/management-api-plugin": "0.13.0"
15
+ },
16
+ "devDependencies": {
17
+ "@types/node": "22.13.10",
18
+ "tsc-watch": "^4.2.9",
19
+ "@flink-app/flink": "0.13.0"
20
+ },
21
+ "gitHead": "4243e3b3cd6d4e1ca001a61baa8436bf2bbe4113",
22
+ "scripts": {
23
+ "test": "echo \"Error: no test specified\"",
24
+ "build": "flink build",
25
+ "clean": "rimraf dist .flink"
26
+ }
27
+ }
package/readme.md CHANGED
@@ -1,79 +1,493 @@
1
- # Firebase messaging plugin
1
+ # Firebase Messaging Plugin
2
2
 
3
- **WORK IN PROGRESS 👷‍♀️👷🏻‍♂️**
3
+ A Flink plugin for sending push notifications using Firebase Cloud Messaging (FCM). Send notifications to individual devices or multiple devices with support for both notification and data messages.
4
4
 
5
- A FLINK plugin used for sending push notifications using Firebase (a.k.a. Firebase Cloud Messaging).
5
+ ## Installation
6
6
 
7
- ## Usage
7
+ Install the plugin to your Flink app project:
8
8
 
9
- Install plugin to your flink app project:
10
-
11
- ```
12
- npm i -S @flink-app/firebase-messaging-plugin
9
+ ```bash
10
+ npm install @flink-app/firebase-messaging-plugin
13
11
  ```
14
12
 
15
- Add and configure plugin in your app startup:
13
+ ## Configuration
16
14
 
17
- ```
15
+ ### Basic Setup
16
+
17
+ Configure the plugin with your Firebase service account credentials:
18
+
19
+ ```typescript
20
+ import { FlinkApp, FlinkContext } from "@flink-app/flink";
18
21
  import { firebaseMessagingPlugin } from "@flink-app/firebase-messaging-plugin";
19
22
 
20
23
  function start() {
21
24
  new FlinkApp<AppContext>({
22
25
  name: "My app",
23
26
  plugins: [
24
- firebaseMessagingPlugin({
25
- serverKey: "YOUR FIREBASE SERVER KEY"
26
- })
27
+ firebaseMessagingPlugin({
28
+ serviceAccountKey: process.env.FIREBASE_SERVICE_ACCOUNT_KEY_BASE64!,
29
+ exposeEndpoints: false // Optional: expose POST /send-message endpoint
30
+ })
27
31
  ],
28
32
  }).start();
29
33
  }
30
-
31
34
  ```
32
35
 
33
- Add it to your app context (normally `Ctx.ts` in the root folder of your project)
36
+ **Plugin Options:**
34
37
 
38
+ ```typescript
39
+ interface FirebaseMessagingPluginOptions {
40
+ serviceAccountKey: string; // Base64-encoded Firebase service account JSON
41
+ exposeEndpoints?: boolean; // Optional: expose HTTP endpoint (default: undefined)
42
+ permissions?: {
43
+ send: string; // Optional: permission required for endpoint
44
+ };
45
+ }
35
46
  ```
47
+
48
+ ### Getting Your Service Account Key
49
+
50
+ 1. Go to Firebase Console > Project Settings > Service Accounts
51
+ 2. Click "Generate New Private Key"
52
+ 3. Download the JSON file
53
+ 4. Base64 encode the JSON file contents:
54
+ ```bash
55
+ cat service-account.json | base64
56
+ ```
57
+ 5. Store the base64 string in your environment variable
58
+
59
+ ## TypeScript Setup
60
+
61
+ Add the plugin context to your app's context type (usually in `Ctx.ts`):
62
+
63
+ ```typescript
64
+ import { FlinkContext } from "@flink-app/flink";
36
65
  import { FirebaseMessagingContext } from "@flink-app/firebase-messaging-plugin";
37
66
 
38
67
  export interface Ctx extends FlinkContext<FirebaseMessagingContext> {
39
- ....
68
+ // Your other context properties
40
69
  }
70
+ ```
41
71
 
72
+ **Plugin Context Interface:**
73
+
74
+ ```typescript
75
+ interface FirebaseMessagingContext {
76
+ firebaseMessaging: {
77
+ send: (message: Message) => void;
78
+ };
79
+ }
42
80
  ```
43
81
 
44
- ## Configuration
82
+ ## Usage in Handlers
83
+
84
+ ### Basic Notification
45
85
 
46
- - `serverKey` - The firebase server key
86
+ Send a simple push notification to a single device:
47
87
 
88
+ ```typescript
89
+ import { Handler } from "@flink-app/flink";
90
+ import { Ctx } from "../Ctx";
48
91
 
49
- ## Use as a managementmodule in the management-api-plugin
92
+ const SendNotification: Handler<Ctx, any, any> = async ({ ctx, req }) => {
93
+ await ctx.plugins.firebaseMessaging.send({
94
+ to: ["device_token_here"],
95
+ notification: {
96
+ title: "New Message",
97
+ body: "You have received a new message"
98
+ },
99
+ data: {}
100
+ });
50
101
 
51
- Initiate the module and configure it:
102
+ return { data: { success: true } };
103
+ };
52
104
 
105
+ export default SendNotification;
53
106
  ```
54
- import { GetManagementModule as GetNotificationManagementModule } from "@flink-app/firebase-messaging-plugin"
55
- const notificationManagementModule = GetNotificationManagementModule({
56
- ui: true,
57
- uiSettings: {
58
- title: "Notifications"
107
+
108
+ ### Notification to Multiple Devices
109
+
110
+ Send the same notification to multiple devices:
111
+
112
+ ```typescript
113
+ await ctx.plugins.firebaseMessaging.send({
114
+ to: [
115
+ "device_token_1",
116
+ "device_token_2",
117
+ "device_token_3"
118
+ ],
119
+ notification: {
120
+ title: "System Update",
121
+ body: "A new version is available"
59
122
  },
60
- segments : [{
61
- id : "all",
62
- description : "All app users",
63
- handler : async (ctx : Ctx) => {
64
- const users = await ctx.repos.userRepo.findAll({})
65
- return users.map(u=>({
66
- userId : u._id.toString(),
67
- pushToken : u.pushNotificationTokens.map(p=>p.token)
68
- }))
123
+ data: {
124
+ version: "2.0.0",
125
+ updateUrl: "https://example.com/update"
126
+ }
127
+ });
128
+ ```
129
+
130
+ ### Data-Only Message
131
+
132
+ Send a data message without notification (silent push):
133
+
134
+ ```typescript
135
+ await ctx.plugins.firebaseMessaging.send({
136
+ to: ["device_token"],
137
+ data: {
138
+ type: "sync",
139
+ timestamp: Date.now().toString(),
140
+ action: "refresh_data"
141
+ }
142
+ });
143
+ ```
144
+
145
+ ### Rich Notification with Custom Data
146
+
147
+ ```typescript
148
+ const SendOrderUpdate: Handler<Ctx, any, any> = async ({ ctx, req }) => {
149
+ const { userId, orderId, status } = req.body;
150
+
151
+ // Get user's device tokens from database
152
+ const user = await ctx.repos.userRepo.findById(userId);
153
+ const deviceTokens = user.pushNotificationTokens || [];
154
+
155
+ await ctx.plugins.firebaseMessaging.send({
156
+ to: deviceTokens,
157
+ notification: {
158
+ title: "Order Update",
159
+ body: `Your order #${orderId} is now ${status}`
69
160
  },
70
-
71
- }],
72
- data : [],
73
- })
161
+ data: {
162
+ orderId: orderId,
163
+ status: status,
164
+ screen: "order_details",
165
+ timestamp: Date.now().toString()
166
+ }
167
+ });
168
+
169
+ return { data: { sent: true } };
170
+ };
171
+ ```
172
+
173
+ ## API Reference
174
+
175
+ ### Message Type
176
+
177
+ ```typescript
178
+ interface Message {
179
+ to: string[]; // Array of FCM device tokens
180
+
181
+ notification?: {
182
+ title?: string; // Notification title
183
+ body?: string; // Notification body text
184
+ };
185
+
186
+ data: { [key: string]: string }; // Custom data payload (all values must be strings)
187
+ }
74
188
  ```
75
189
 
190
+ **Important Notes:**
191
+ - The `data` field is required (can be an empty object `{}`)
192
+ - All values in the `data` object must be strings
193
+ - `notification` is optional - omit it for silent data messages
194
+ - The `send` method processes messages in batches of 500 devices
195
+
196
+ ### Context API
197
+
198
+ ```typescript
199
+ // Send a message
200
+ ctx.plugins.firebaseMessaging.send(message: Message): void
201
+ ```
202
+
203
+ The `send` method:
204
+ - Processes devices in batches of 500 (FCM limitation)
205
+ - Logs success/failure for each device to debug logs
206
+ - Handles errors gracefully without throwing
207
+
208
+ ## Registered Endpoints
209
+
210
+ If `exposeEndpoints` is enabled, the plugin registers:
211
+
212
+ ### POST /send-message
213
+
214
+ Send a push notification via HTTP endpoint.
215
+
216
+ **Request Body:**
217
+ ```typescript
218
+ {
219
+ to: string[]; // Device tokens
220
+ notification?: {
221
+ title?: string;
222
+ body?: string;
223
+ };
224
+ data: { [key: string]: string };
225
+ }
226
+ ```
227
+
228
+ **Response:**
229
+ ```typescript
230
+ {
231
+ data: {
232
+ failedDevices: string[] // Currently returns empty array
233
+ }
234
+ }
235
+ ```
236
+
237
+ **Example:**
238
+ ```bash
239
+ curl -X POST http://localhost:3000/send-message \
240
+ -H "Content-Type: application/json" \
241
+ -d '{
242
+ "to": ["device_token_123"],
243
+ "notification": {
244
+ "title": "Hello",
245
+ "body": "World"
246
+ },
247
+ "data": {}
248
+ }'
249
+ ```
250
+
251
+ **Permissions:**
252
+ - Default permission: `firebase-messaging:send`
253
+ - Can be customized via `permissions.send` option
254
+
255
+ ## Complete Example
256
+
257
+ Here's a complete example showing different notification scenarios:
258
+
259
+ ```typescript
260
+ import { Handler } from "@flink-app/flink";
261
+ import { Ctx } from "../Ctx";
262
+
263
+ interface NotificationRequest {
264
+ userIds: string[];
265
+ type: "message" | "order" | "reminder" | "alert";
266
+ title: string;
267
+ body: string;
268
+ data?: Record<string, any>;
269
+ }
76
270
 
271
+ const SendPushNotification: Handler<Ctx, NotificationRequest, { sent: number }> = async ({
272
+ ctx,
273
+ req
274
+ }) => {
275
+ const { userIds, type, title, body, data = {} } = req.body;
77
276
 
277
+ // Collect device tokens from all users
278
+ const users = await ctx.repos.userRepo.findByIds(userIds);
279
+ const deviceTokens: string[] = [];
280
+
281
+ for (const user of users) {
282
+ if (user.pushTokens && user.pushTokens.length > 0) {
283
+ deviceTokens.push(...user.pushTokens);
284
+ }
285
+ }
286
+
287
+ if (deviceTokens.length === 0) {
288
+ return { data: { sent: 0 } };
289
+ }
290
+
291
+ // Convert data values to strings (FCM requirement)
292
+ const stringData: { [key: string]: string } = {};
293
+ for (const [key, value] of Object.entries(data)) {
294
+ stringData[key] = String(value);
295
+ }
296
+
297
+ // Add metadata
298
+ stringData.type = type;
299
+ stringData.sentAt = new Date().toISOString();
300
+
301
+ // Send notification
302
+ await ctx.plugins.firebaseMessaging.send({
303
+ to: deviceTokens,
304
+ notification: {
305
+ title,
306
+ body
307
+ },
308
+ data: stringData
309
+ });
310
+
311
+ return { data: { sent: deviceTokens.length } };
312
+ };
313
+
314
+ export default SendPushNotification;
315
+ ```
316
+
317
+
318
+ ## Best Practices
319
+
320
+ ### Device Token Management
321
+
322
+ Store and manage device tokens properly:
323
+
324
+ ```typescript
325
+ // When user logs in or registers device
326
+ const RegisterDeviceToken: Handler<Ctx, any, any> = async ({ ctx, req }) => {
327
+ const { userId, deviceToken } = req.body;
328
+
329
+ await ctx.repos.userRepo.update(userId, {
330
+ $addToSet: { pushTokens: deviceToken } // Avoid duplicates
331
+ });
332
+
333
+ return { data: { success: true } };
334
+ };
335
+
336
+ // When user logs out
337
+ const RemoveDeviceToken: Handler<Ctx, any, any> = async ({ ctx, req }) => {
338
+ const { userId, deviceToken } = req.body;
339
+
340
+ await ctx.repos.userRepo.update(userId, {
341
+ $pull: { pushTokens: deviceToken }
342
+ });
343
+
344
+ return { data: { success: true } };
345
+ };
346
+ ```
347
+
348
+ ### Data Payload Conversion
349
+
350
+ Always convert data values to strings:
351
+
352
+ ```typescript
353
+ // Bad - will fail
354
+ data: {
355
+ userId: 123, // Number
356
+ isActive: true, // Boolean
357
+ metadata: { foo: "bar" } // Object
358
+ }
359
+
360
+ // Good - all strings
361
+ data: {
362
+ userId: "123",
363
+ isActive: "true",
364
+ metadata: JSON.stringify({ foo: "bar" })
365
+ }
366
+ ```
367
+
368
+ ### Notification vs Data Messages
369
+
370
+ **Notification Messages:**
371
+ - Display a notification in the system tray
372
+ - Wake up the app when tapped
373
+ - Best for user-facing alerts
374
+
375
+ **Data Messages:**
376
+ - Silent delivery to app
377
+ - App handles processing
378
+ - Best for background sync, state updates
379
+
380
+ ```typescript
381
+ // Notification message (user sees it)
382
+ {
383
+ notification: { title: "New message", body: "John sent you a message" },
384
+ data: { chatId: "123" }
385
+ }
386
+
387
+ // Data message (silent)
388
+ {
389
+ data: { type: "sync", lastSyncTimestamp: "1234567890" }
390
+ }
391
+ ```
392
+
393
+ ### Batch Processing
394
+
395
+ The plugin automatically handles batching (500 devices per batch), but you should still consider:
396
+
397
+ ```typescript
398
+ // Good - let the plugin handle batching
399
+ await ctx.plugins.firebaseMessaging.send({
400
+ to: thousandsOfTokens, // Plugin splits into batches of 500
401
+ notification: { title: "Update", body: "New feature available" },
402
+ data: {}
403
+ });
404
+
405
+ // Also good - send to specific segments
406
+ const activeUsers = await ctx.repos.userRepo.findActive();
407
+ const tokens = activeUsers.flatMap(u => u.pushTokens || []);
408
+ await ctx.plugins.firebaseMessaging.send({
409
+ to: tokens,
410
+ notification: { title: "Hello active users!", body: "..." },
411
+ data: {}
412
+ });
413
+ ```
414
+
415
+ ### Error Handling
416
+
417
+ The plugin logs errors internally but doesn't throw. Monitor logs:
418
+
419
+ ```typescript
420
+ // The plugin logs:
421
+ // - Success: "[firebaseMessaging] Successfully sent to device {token}"
422
+ // - Failure: "[firebaseMessaging] Failed sending to device {token}: {error}"
423
+ // - Batch error: "[firebaseMessaging] Failed sending batch: {error}"
424
+
425
+ // You can wrap sends with try-catch if needed
426
+ try {
427
+ await ctx.plugins.firebaseMessaging.send({
428
+ to: deviceTokens,
429
+ notification: { title: "Test", body: "Test" },
430
+ data: {}
431
+ });
432
+ } catch (error) {
433
+ console.error("Unexpected error:", error);
434
+ }
435
+ ```
436
+
437
+ ## Troubleshooting
438
+
439
+ ### Notifications Not Received
440
+
441
+ 1. **Invalid device tokens** - Tokens expire or become invalid
442
+ - Implement token refresh on the client side
443
+ - Remove invalid tokens from your database
444
+
445
+ 2. **Service account permissions** - Verify the service account has FCM permissions
446
+ - Check Firebase Console > Project Settings > Service Accounts
447
+ - Ensure "Firebase Admin SDK" role is granted
448
+
449
+ 3. **Base64 encoding** - Ensure service account key is properly encoded
450
+ ```bash
451
+ # Verify decoding works
452
+ echo $FIREBASE_SERVICE_ACCOUNT_KEY_BASE64 | base64 -d
453
+ ```
454
+
455
+ 4. **App not running** - Data messages require the app to be running
456
+ - Use notification messages to wake the app
457
+ - Combine notification + data for best results
458
+
459
+ ### Invalid Data Format
460
+
461
+ Error: "All data values must be strings"
462
+
463
+ ```typescript
464
+ // Fix: Convert all values to strings
465
+ data: {
466
+ count: String(42),
467
+ timestamp: new Date().toISOString(),
468
+ metadata: JSON.stringify({ key: "value" })
469
+ }
470
+ ```
471
+
472
+ ### Check Debug Logs
473
+
474
+ Enable Flink debug mode to see detailed FCM logs:
475
+
476
+ ```typescript
477
+ new FlinkApp<Ctx>({
478
+ name: "My app",
479
+ debug: true, // Enable debug logging
480
+ plugins: [
481
+ firebaseMessagingPlugin({ ... })
482
+ ]
483
+ }).start();
484
+ ```
78
485
 
486
+ ## Notes
79
487
 
488
+ - Messages are processed in batches of 500 devices (FCM limitation)
489
+ - All data values must be strings (numbers, booleans, objects must be converted)
490
+ - The plugin uses Firebase Admin SDK v11+
491
+ - Device tokens should be stored securely and updated regularly
492
+ - Invalid/expired tokens are logged but don't stop delivery to other devices
493
+ - The `send` method is fire-and-forget (doesn't wait for delivery confirmation)
package/src/index.ts CHANGED
@@ -1,4 +1,3 @@
1
1
  export * from "./firebaseMessagingPlugin";
2
2
  export * from "./FirebaseMessagingContext";
3
3
  export * from "./ManagementModule";
4
-
package/tsconfig.json CHANGED
@@ -15,7 +15,7 @@
15
15
  "noEmit": false,
16
16
  "declaration": true,
17
17
  "experimentalDecorators": true,
18
- "checkJs": true,
18
+ "checkJs": false,
19
19
  "outDir": "dist"
20
20
  },
21
21
  "include": ["./src/**/*.ts", "./.flink/**/*"],