capacitor-messenger-notifications 1.0.0 → 1.0.2

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 (41) hide show
  1. package/MessengerNotifications.podspec +2 -1
  2. package/Package.swift +4 -2
  3. package/README.md +344 -59
  4. package/android/build.gradle +12 -2
  5. package/android/src/main/AndroidManifest.xml +19 -0
  6. package/android/src/main/java/com/codecraft_studio/messenger/notifications/EncryptedMessageNotifier.java +689 -40
  7. package/android/src/main/java/com/codecraft_studio/messenger/notifications/FcmFetchBackgroundService.java +31 -0
  8. package/android/src/main/java/com/codecraft_studio/messenger/notifications/FcmFetchForegroundService.java +177 -0
  9. package/android/src/main/java/com/codecraft_studio/messenger/notifications/FcmFetchManager.java +245 -3
  10. package/android/src/main/java/com/codecraft_studio/messenger/notifications/FcmJobService.java +62 -0
  11. package/android/src/main/java/com/codecraft_studio/messenger/notifications/FcmTokenRegistrar.java +142 -0
  12. package/android/src/main/java/com/codecraft_studio/messenger/notifications/GmsHelper.java +20 -0
  13. package/android/src/main/java/com/codecraft_studio/messenger/notifications/MessageFlowLogger.java +151 -0
  14. package/android/src/main/java/com/codecraft_studio/messenger/notifications/MessengerNotificationsPlugin.java +206 -15
  15. package/android/src/main/java/com/codecraft_studio/messenger/notifications/NativeCrypto.java +129 -27
  16. package/android/src/main/java/com/codecraft_studio/messenger/notifications/NotificationDismissReceiver.java +4 -5
  17. package/android/src/main/java/com/codecraft_studio/messenger/notifications/NotificationHelper.java +420 -56
  18. package/android/src/main/java/com/codecraft_studio/messenger/notifications/PersistentSocketService.java +116 -30
  19. package/android/src/main/java/com/codecraft_studio/messenger/notifications/TemporarySocketSessionManager.java +528 -0
  20. package/android/src/main/java/com/codecraft_studio/messenger/notifications/UnreadMessagesFetcher.java +220 -0
  21. package/android/src/main/res/drawable/ic_notification.png +0 -0
  22. package/android/src/main/res/drawable/ic_transparent.png +0 -0
  23. package/android/src/main/res/values/strings.xml +4 -0
  24. package/dist/esm/definitions.d.ts +7 -0
  25. package/dist/esm/definitions.js.map +1 -1
  26. package/dist/esm/web.d.ts +3 -0
  27. package/dist/esm/web.js +3 -0
  28. package/dist/esm/web.js.map +1 -1
  29. package/dist/plugin.cjs.js +3 -0
  30. package/dist/plugin.cjs.js.map +1 -1
  31. package/dist/plugin.js +3 -0
  32. package/dist/plugin.js.map +1 -1
  33. package/ios/Sources/MessengerNotificationsPlugin/EncryptedMessageNotifier.swift +362 -0
  34. package/ios/Sources/MessengerNotificationsPlugin/FcmTokenRegistrar.swift +228 -0
  35. package/ios/Sources/MessengerNotificationsPlugin/MessengerNotificationsPlugin.swift +45 -16
  36. package/ios/Sources/MessengerNotificationsPlugin/NativeCrypto.swift +73 -14
  37. package/ios/Sources/MessengerNotificationsPlugin/NotificationHelper.swift +777 -40
  38. package/ios/Sources/MessengerNotificationsPlugin/SafeStorageStore.swift +229 -5
  39. package/ios/Sources/MessengerNotificationsPlugin/TemporarySocketSessionManager.swift +1955 -57
  40. package/ios/Sources/MessengerNotificationsPlugin/UnreadMessagesFetcher.swift +544 -0
  41. package/package.json +1 -1
@@ -13,6 +13,7 @@ Pod::Spec.new do |s|
13
13
  s.source_files = 'ios/Sources/**/*.{swift,h,m,c,cc,mm,cpp}'
14
14
  s.ios.deployment_target = '13.0'
15
15
  s.dependency 'Capacitor'
16
- s.dependency 'Socket.io-client-swift', '~> 15.0'
16
+ s.dependency 'Socket.io-client-swift', '~> 16.0'
17
+ s.dependency 'Sodium', '~> 0.9'
17
18
  s.swift_version = '5.1'
18
19
  end
package/Package.swift CHANGED
@@ -11,7 +11,8 @@ let package = Package(
11
11
  ],
12
12
  dependencies: [
13
13
  .package(url: "https://github.com/ionic-team/capacitor-swift-pm.git", from: "7.1.0"),
14
- .package(url: "https://github.com/socketio/socket.io-client-swift.git", from: "16.1.0")
14
+ .package(url: "https://github.com/socketio/socket.io-client-swift.git", from: "16.1.0"),
15
+ .package(url: "https://github.com/jedisct1/swift-sodium.git", from: "0.9.1")
15
16
  ],
16
17
  targets: [
17
18
  .target(
@@ -19,7 +20,8 @@ let package = Package(
19
20
  dependencies: [
20
21
  .product(name: "Capacitor", package: "capacitor-swift-pm"),
21
22
  .product(name: "Cordova", package: "capacitor-swift-pm"),
22
- .product(name: "SocketIO", package: "socket.io-client-swift")
23
+ .product(name: "SocketIO", package: "socket.io-client-swift"),
24
+ .product(name: "Sodium", package: "swift-sodium")
23
25
  ],
24
26
  path: "ios/Sources/MessengerNotificationsPlugin")
25
27
  ]
package/README.md CHANGED
@@ -10,23 +10,43 @@ Capacitor plugin for managing messenger-style notifications with WebSocket suppo
10
10
  - [Requirements](#requirements)
11
11
  - [Install](#install)
12
12
  - [Usage](#usage)
13
- - [JavaScript Examples](#javascript-examples)
14
- - [Native Integration (Android/iOS)](#native-integration-androidios)
15
- - [Configuration](#configuration)
16
- - [Platform Implementation](#platform-implementation)
13
+ - [Permissions](#permissions)
14
+ - [Show a Notification](#show-a-notification)
15
+ - [Clear a Room's Notifications](#clear-a-rooms-notifications)
16
+ - [Cold-Start Navigation](#cold-start-navigation)
17
+ - [Persistent Socket (Android)](#persistent-socket-android)
18
+ - [Push token / OneSignal registration](#push-token--onesignal-registration)
19
+ - [Native Integration](#native-integration)
17
20
  - [Setup by Platform](#setup-by-platform)
21
+ - [Android](#android)
22
+ - [iOS](#ios)
23
+ - [How It Works](#how-it-works)
18
24
  - [API](#api)
19
25
  - [Troubleshooting](#troubleshooting)
20
26
  - [Development](#development)
21
27
  - [Contributing](#contributing)
22
28
  - [License](#license)
23
29
 
30
+ ---
31
+
24
32
  ## Features
25
33
 
26
- - **Native Notification Grouping**: Automatically groups messages by room ID using `MessagingStyle` (Android) and `threadIdentifier` (iOS).
27
- - **Persistent WebSocket (Android)**: Foreground service that stays alive to receive messages even when the app is killed.
28
- - **Background Fetch (iOS)**: Automatically wakes up to fetch unread messages when a silent push is received.
29
- - **End-to-End Encryption Ready**: Designed to handle encrypted payloads and decrypt them on-device before showing notifications.
34
+ - **Native Notification Grouping**: Groups messages per room using `MessagingStyle` (Android) and `threadIdentifier` (iOS), keyed dynamically off the host app's package/bundle ID.
35
+ - **End-to-End Decryption**: Decrypts room names, usernames, and message bodies on-device using Libsodium (Android) and swift-sodium (iOS) before showing any notification.
36
+ - **Persistent WebSocket (Android)**: A foreground service maintains a live Socket.IO connection for devices without Google Play Services (GMS), auto-reconnecting on token or network changes.
37
+ - **FCM Background Fetch (Android)**: On GMS devices, `FcmFetchForegroundService` + `FcmFetchBackgroundService` spin up a short-lived socket session triggered by each FCM push and tear it down cleanly.
38
+ - **Job Scheduler Retry (Android)**: `FcmJobService` schedules a retry via `JobScheduler` if the primary fetch fails, ensuring no message is dropped.
39
+ - **Background Fetch (iOS)**: `TemporarySocketSessionManager` opens a short-lived Socket.IO session inside `didReceiveRemoteNotification` with idle and max-session timeouts.
40
+ - **HTTP Unread Fallback**: Both platforms fall back to fetching from the `/api/rooms/messages/unread` endpoint when a socket session cannot decrypt a message.
41
+ - **Rich Notifications**: Custom SVG avatar rendering (Android via AndroidSVG), conversation shortcuts, and dismissal tracking.
42
+ - **Notification Deduplication**: In-memory + persisted message ID tracking prevents showing the same message twice across socket and FCM paths.
43
+ - **Dynamic Resources**: All icon, app name, and notification group key lookups are resolved from the host app at runtime — no hardcoded app-specific values.
44
+ - **`window.Notification` Polyfill (Android)**: Injects a polyfill into the WebView so the JS app's `new Notification(...)` calls route through the native plugin.
45
+ - **Push registration**: **Android** — registers the FCM token via `POST …/api/users/fcm-token` (or your `fcmTokenEndpoint` override). **iOS** — can register **both**: FCM token to the same endpoint when `fcmToken` is in `safe_storage` (e.g. from Firebase), and the OneSignal subscription id via `POST …/api/push/register` when `onesignalPlayerId` is set (`FcmTokenRegistrar.updateOneSignalPlayerId(_:)` / `updateFcmToken(_:)`).
46
+ - **Message flow logging (optional)**: **Android** — `MessageFlowLogger` POSTs structured steps to `{backend}/api/message-flow-logs/ingest`. **iOS** — `MessageFlowLogger` in the plugin sends the same style of events over HTTPS.
47
+ - **App Group storage (iOS)**: `SafeStorageStore` can mirror `safe_storage` into a shared App Group when you set the `MessengerNotificationsAppGroup` key in Info.plist (for extensions / NSE).
48
+
49
+ ---
30
50
 
31
51
  ## Requirements
32
52
 
@@ -35,6 +55,8 @@ Capacitor plugin for managing messenger-style notifications with WebSocket suppo
35
55
  - **iOS**: 13.0 or higher
36
56
  - **Android**: API level 22 (Android 5.1) or higher
37
57
 
58
+ ---
59
+
38
60
  ## Install
39
61
 
40
62
  ```bash
@@ -42,111 +64,374 @@ npm install capacitor-messenger-notifications
42
64
  npx cap sync
43
65
  ```
44
66
 
67
+ ---
68
+
45
69
  ## Usage
46
70
 
47
- ### JavaScript Examples
71
+ ### Permissions
48
72
 
49
- #### Start Persistent Socket (Android)
73
+ Before showing notifications, check and request permission:
50
74
 
51
75
  ```typescript
52
76
  import { MessengerNotifications } from 'capacitor-messenger-notifications';
53
77
 
54
- // Starts a foreground service on Android to maintain a heartbeat connection
55
- await MessengerNotifications.startPersistentSocket({
56
- url: 'wss://your-chat-server.com',
57
- token: 'YOUR_AUTH_TOKEN'
58
- });
78
+ const status = await MessengerNotifications.checkPermissions();
79
+
80
+ if (status.notifications !== 'granted') {
81
+ const result = await MessengerNotifications.requestPermissions();
82
+ if (result.notifications !== 'granted') {
83
+ console.warn('Notification permission denied');
84
+ }
85
+ }
59
86
  ```
60
87
 
61
- #### Show a Manual Notification
88
+ ### Show a Notification
62
89
 
63
90
  ```typescript
64
91
  await MessengerNotifications.showNotification({
65
- title: 'John Doe',
92
+ title: 'Alice',
66
93
  body: 'Hey, how are you?',
67
94
  roomId: 101,
68
95
  roomName: 'General Chat',
69
96
  messageId: 'uuid-12345',
70
- timestamp: Date.now()
97
+ timestamp: Date.now(),
98
+ senderId: 42,
99
+ avatarSvg: '<svg>...</svg>', // optional SVG string for sender avatar
71
100
  });
72
101
  ```
73
102
 
74
- ### Native Integration (Android/iOS)
103
+ ### Clear a Room's Notifications
75
104
 
76
- This plugin is often triggered from native background tasks (FCM / Silent Push).
77
-
78
- - **Android**: Use `EncryptedMessageNotifier.notifyFromSocketPayload(context, data)` from your background services.
79
- - **iOS**: Use `TemporarySocketSessionManager.shared.fetchAndNotify(...)` inside `didReceiveRemoteNotification`.
105
+ ```typescript
106
+ await MessengerNotifications.clearRoomNotification({ roomId: 101 });
107
+ ```
80
108
 
81
- ## Configuration
109
+ ### Cold-Start Navigation
82
110
 
83
- Most configurations are handled dynamically via the API, but you can define defaults in **`capacitor.config.json`**:
111
+ Notification tap intents include a `roomId` extra on Android. The plugin exposes `getPendingRoomId()` for JS; on **Android** you should also forward the launch intent room into native storage if you use a custom activity, e.g. `NotificationHelper.setPendingRoomId(roomId)` when handling `onNewIntent` / `onCreate`. On **iOS**, set `MessengerNotificationsPlugin.pendingRoomId = roomId` from your `AppDelegate` / OneSignal click handler when the user opens a chat from a notification.
84
112
 
85
- ```json
86
- {
87
- "plugins": {
88
- "MessengerNotifications": {
89
- "defaultSocketUrl": "wss://your-default-server.com",
90
- "notificationChannelId": "chat_messages",
91
- "notificationChannelName": "Messenger Notifications"
92
- }
93
- }
113
+ ```typescript
114
+ const { roomId } = await MessengerNotifications.getPendingRoomId();
115
+ if (roomId !== null) {
116
+ router.push(`/room/${roomId}`);
94
117
  }
95
118
  ```
96
119
 
97
- ## Platform Implementation
120
+ ### Persistent Socket (Android)
98
121
 
99
- | Platform | Implementation |
100
- | --- | --- |
101
- | Android | Foreground Service + NotificationManager with `MessagingStyle`. |
102
- | iOS | `UNNotificationContent` with `threadIdentifier` + SocketIO Task. |
122
+ On non-GMS Android devices the plugin starts a foreground service that keeps a Socket.IO connection alive. Call this after the user logs in:
103
123
 
104
- ## Setup by Platform
124
+ ```typescript
125
+ await MessengerNotifications.startPersistentSocket({
126
+ url: 'wss://your-chat-server.com',
127
+ token: 'YOUR_AUTH_JWT',
128
+ });
129
+
130
+ // Call when the user logs out
131
+ await MessengerNotifications.stopPersistentSocket();
132
+ ```
133
+
134
+ > On GMS devices the socket is only opened for the duration of each incoming FCM push. `startPersistentSocket` stores the credentials but does not start the service.
135
+
136
+ ### Push token / OneSignal registration
137
+
138
+ After the user logs in, call:
139
+
140
+ ```typescript
141
+ await MessengerNotifications.registerFcmToken();
142
+ ```
143
+
144
+ - **Android**: Reads `fcmToken`, JWT, and backend URL from `safe_storage`, then `POST {base}/api/users/fcm-token` with `{ "fcmToken": "..." }` (or your `fcmTokenEndpoint` override). Skips when `fcmTokenRegistered` is already true.
145
+ - **iOS**: Runs **both** registrations when the prerequisites exist (each is independent). **FCM**: `fcmToken` + JWT + base URL → `POST {base}/api/users/fcm-token` (or `fcmTokenEndpoint`); skips when `fcmTokenRegistered` is true. **OneSignal**: `onesignalPlayerId` + JWT + base URL → `POST {base}/api/push/register` with `{ "playerId": "...", "platform": "ios" }`; skips when `onesignalPlayerIdRegistered` is true. From Swift use `FcmTokenRegistrar.updateFcmToken(_:)` / `updateOneSignalPlayerId(_:)` when tokens change.
105
146
 
106
- > For full host‑app steps (Android & iOS), see `HOST_APP_SETUP.md`.
147
+ ### Native Integration
148
+
149
+ The plugin is typically triggered from native background handlers:
150
+
151
+ **Android** — Inside your `FirebaseMessagingService.onMessageReceived`:
152
+
153
+ ```java
154
+ // Kicks off the full fetch → decrypt → notify pipeline
155
+ FcmFetchManager.retrieveMessages(context, remoteMessage.getData());
156
+ ```
157
+
158
+ **iOS** — Inside `application(_:didReceiveRemoteNotification:fetchCompletionHandler:)`:
159
+
160
+ ```swift
161
+ EncryptedMessageNotifier.notifyFromPushData(userInfo as? [String: Any] ?? [:])
162
+ ```
163
+
164
+ ---
165
+
166
+ ## Setup by Platform
107
167
 
108
168
  ### Android
109
169
 
110
- - **AndroidManifest.xml**: Add `FOREGROUND_SERVICE` and `POST_NOTIFICATIONS` permissions.
111
- - **ProGuard**: Add `-keep` rules for the plugin and Socket.IO.
170
+ #### 1. Safe Storage Keys
171
+
172
+ The plugin reads credentials from `SharedPreferences` file `"safe_storage"`. Your JS app should write the following keys (e.g. via a SafeStorage plugin):
173
+
174
+ | Key | Description |
175
+ | --- | --- |
176
+ | `token` / `authToken` | User's JWT for socket auth and API calls |
177
+ | `socketUrl` | WebSocket server URL |
178
+ | `backendBaseUrl` / `serverUrl` / ... | HTTP base URL for the unread messages API |
179
+ | `fcmToken` | FCM registration token (written by Firebase) |
180
+ | `fcmTokenRegistered` | Set by the plugin to true after a successful `POST` to the FCM endpoint |
181
+ | `fcmTokenEndpoint` | Optional path override (default `/api/users/fcm-token`) |
182
+ | `roomDecryptedKeys` | JSON map of room E2EE private keys |
183
+ | `memberDecryptedKeys` | JSON map of member E2EE private keys |
184
+
185
+ #### 2. Required Resources
186
+
187
+ Add the following drawables to your host app's `res/drawable/` so the plugin can find them at runtime (fallback to system icons if absent):
188
+
189
+ | Resource | Purpose |
190
+ | --- | --- |
191
+ | `ic_notification.png` | Small notification icon (monochrome, white on transparent) |
192
+ | `ic_transparent.png` | Transparent icon for the persistent service notification |
193
+
194
+ #### 3. AndroidManifest.xml
195
+
196
+ The plugin's manifest already declares all necessary services and permissions. If you need to override, merge the following into your app's manifest:
197
+
198
+ ```xml
199
+ <uses-permission android:name="android.permission.INTERNET" />
200
+ <uses-permission android:name="android.permission.WAKE_LOCK" />
201
+ <uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
202
+ <uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC" />
203
+ <uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
204
+ <uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
205
+ ```
206
+
207
+ #### 4. ProGuard
208
+
209
+ Add to your `proguard-rules.pro`:
210
+
211
+ ```proguard
212
+ -keep class com.codecraft_studio.messenger.notifications.** { *; }
213
+ -keep class io.socket.** { *; }
214
+ -keep class org.java_websocket.** { *; }
215
+ ```
216
+
217
+ ---
112
218
 
113
219
  ### iOS
114
220
 
115
- - **Capabilities**: Enable **Push Notifications** and **Background Modes** (Background fetch, Remote notifications).
221
+ #### 1. Capabilities
116
222
 
117
- ## API
223
+ In Xcode, enable the following for your app target:
224
+
225
+ - **Push Notifications**
226
+ - **Background Modes** → check *Remote notifications* and *Background fetch*
227
+
228
+ #### 2. Info.plist (App Group, optional)
229
+
230
+ If you use a Notification Service Extension or shared storage with the main app, set your App Group identifier:
231
+
232
+ ```xml
233
+ <key>MessengerNotificationsAppGroup</key>
234
+ <string>group.your.bundle.app</string>
235
+ ```
236
+
237
+ If omitted, the plugin uses only `UserDefaults.standard` for the `safe_storage` dictionary.
118
238
 
119
- | Method | Description |
239
+ #### 3. AppDelegate
240
+
241
+ Forward remote notifications to the plugin when you handle data/silent payloads. When the user taps a notification and you know the `roomId`, assign `MessengerNotificationsPlugin.pendingRoomId = roomId` so JavaScript can read it via `getPendingRoomId()`.
242
+
243
+ ```swift
244
+ import UIKit
245
+ import Capacitor
246
+
247
+ @UIApplicationMain
248
+ class AppDelegate: UIResponder, UIApplicationDelegate {
249
+
250
+ func application(
251
+ _ application: UIApplication,
252
+ didReceiveRemoteNotification userInfo: [AnyHashable: Any],
253
+ fetchCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void
254
+ ) {
255
+ let data = userInfo as? [String: Any] ?? [:]
256
+ let handled = EncryptedMessageNotifier.notifyFromPushData(data)
257
+ completionHandler(handled ? .newData : .noData)
258
+ }
259
+ }
260
+ ```
261
+
262
+ #### 4. Safe Storage Keys
263
+
264
+ The plugin reads from `UserDefaults` suite `"safe_storage"` (and mirrors to the App Group suite when configured). Write the following keys from your JS app:
265
+
266
+ | Key | Description |
120
267
  | --- | --- |
121
- | `startPersistentSocket(options)` | Starts the background socket service (Android only). |
122
- | `stopPersistentSocket()` | Stops the background socket service. |
123
- | `showNotification(options)` | Manually triggers a native grouped notification. |
124
- | `clearRoomNotification(options)` | Clears all notifications for a specific room. |
125
- | `getPendingRoomId()` | Returns the roomId if the app was launched from a notification. |
268
+ | `token` / `authToken` | User's JWT |
269
+ | `socketUrl` | WebSocket server URL |
270
+ | `backendBaseUrl` / `serverUrl` / ... | HTTP base URL |
271
+ | `onesignalPlayerId` / `onesignalSubscriptionId` | OneSignal subscription id (`POST /api/push/register`) |
272
+ | `onesignalPlayerIdRegistered` | `"true"` after OneSignal id is ACKed by your backend (plugin sets this) |
273
+ | `fcmToken` | FCM registration token from Firebase (optional; `POST /api/users/fcm-token` same as Android) |
274
+ | `fcmTokenRegistered` | `"true"` after FCM token is ACKed (plugin sets this) |
275
+ | `fcmTokenEndpoint` | Optional path override for FCM registration (default `/api/users/fcm-token`) |
276
+ | `roomDecryptedKeys` | JSON map of room E2EE private keys |
277
+ | `memberDecryptedKeys` | JSON map of member E2EE private keys |
278
+
279
+ ---
280
+
281
+ ## How It Works
282
+
283
+ ### Message Flow (Android)
284
+
285
+ ```text
286
+ FCM push received
287
+
288
+
289
+ FcmFetchManager.retrieveMessages()
290
+
291
+ ├─► FcmFetchForegroundService (keep process alive + wake lock)
292
+
293
+ ├─► TemporarySocketSessionManager.runSession()
294
+ │ └─► socket.emit("sync_messages")
295
+ │ └─► EncryptedMessageNotifier.notifyFromSyncMessagesResponse()
296
+ │ └─► NativeCrypto.decrypt*()
297
+ │ └─► NotificationHelper.showRoomNotification()
298
+
299
+ └─► [fallback] UnreadMessagesFetcher.fetchAndNotify() (HTTP API)
300
+ ```
301
+
302
+ ### Message Flow (iOS)
303
+
304
+ ```text
305
+ Silent push received
306
+
307
+
308
+ EncryptedMessageNotifier.notifyFromPushData()
309
+
310
+ ├─► Direct decrypt from push payload
311
+
312
+ └─► [fallback] TemporarySocketSessionManager.runSession()
313
+ └─► UnreadMessagesFetcher.fetchAndNotify() (HTTP API)
314
+ ```
315
+
316
+ ---
317
+
318
+ ## API
319
+
320
+ ### `showNotification(options)`
321
+
322
+ Shows a native grouped chat notification.
323
+
324
+ | Option | Type | Required | Description |
325
+ | ----------- | -------- | -------- | ---------------------------------------------------------- |
326
+ | `title` | `string` | ✓ | Sender name displayed in the notification |
327
+ | `body` | `string` | ✓ | Message body |
328
+ | `roomId` | `number` | ✓ | Room identifier used for grouping |
329
+ | `messageId` | `string` | | Unique message ID for deduplication |
330
+ | `timestamp` | `number` | | Unix timestamp (ms) for message ordering |
331
+ | `roomName` | `string` | | Room display name shown in notification subtitle |
332
+ | `senderId` | `number` | | Sender's user ID (used for avatar/Person identity) |
333
+ | `avatarSvg` | `string` | | SVG string for the sender's avatar |
334
+
335
+ ---
336
+
337
+ ### `clearRoomNotification(options)`
338
+
339
+ Cancels all active notifications for a room and clears its in-memory history.
340
+
341
+ | Option | Type | Required | Description |
342
+ | -------- | -------- | -------- | -------------------------------------------- |
343
+ | `roomId` | `number` | ✓ | Room whose notifications should be cleared |
344
+
345
+ ---
346
+
347
+ ### `getPendingRoomId()`
348
+
349
+ Returns the `roomId` from the notification that launched the app (cold start), then clears it. Returns `{ roomId: null }` if the app was not opened via a notification.
350
+
351
+ ---
352
+
353
+ ### `startPersistentSocket(options)`
354
+
355
+ Stores socket credentials and starts the persistent foreground service on non-GMS Android devices.
356
+
357
+ | Option | Type | Required | Description |
358
+ | ------- | -------- | -------- | -------------------- |
359
+ | `url` | `string` | ✓ | WebSocket server URL |
360
+ | `token` | `string` | ✓ | JWT auth token |
361
+
362
+ ---
363
+
364
+ ### `stopPersistentSocket()`
365
+
366
+ Stops the persistent socket foreground service.
367
+
368
+ ---
369
+
370
+ ### `checkPermissions()`
371
+
372
+ Returns the current notification permission state.
373
+
374
+ **Returns**: `Promise<{ notifications: 'granted' | 'denied' | 'prompt' }>`
375
+
376
+ ---
377
+
378
+ ### `requestPermissions()`
379
+
380
+ Prompts the user for notification permission.
381
+
382
+ **Returns**: `Promise<{ notifications: 'granted' | 'denied' | 'prompt' }>`
383
+
384
+ ---
385
+
386
+ ### `registerFcmToken()`
387
+
388
+ **Android**: Registers `fcmToken` with `{base}/api/users/fcm-token` (or `fcmTokenEndpoint`). **iOS**: Registers **FCM** (`fcmToken` → same endpoint/shape as Android) and **OneSignal** (`onesignalPlayerId` → `{base}/api/push/register`) when each is present and not yet marked registered. Reads JWT and backend URL from `safe_storage`.
126
389
 
127
390
  ---
128
391
 
129
392
  ## Troubleshooting
130
393
 
131
- ### Android: Service is killed
394
+ ### Android: Notifications not appearing after FCM push
395
+
396
+ 1. Confirm `roomDecryptedKeys` and `memberDecryptedKeys` are written to `safe_storage` before the push arrives.
397
+ 2. Check Logcat for `EncryptedMessageNotifier`, `NativeCrypto`, and `NotificationHelper` tags.
398
+ 3. Ensure `ic_notification` drawable exists in the host app (required for the notification icon).
399
+
400
+ ### Android: Persistent service is killed
401
+
402
+ Ensure `android:foregroundServiceType="dataSync"` is declared on `PersistentSocketService` (already set in the plugin manifest). On Android 14+ the system enforces this.
403
+
404
+ ### Android: Duplicate notifications
405
+
406
+ The plugin deduplicates by `messageId`. Ensure each message has a unique, stable `messageId` in the FCM payload.
407
+
408
+ ### iOS: Notifications not grouping
132
409
 
133
- Ensure you have added `android:foregroundServiceType="dataSync"` to the service declaration in `AndroidManifest.xml` as required by Android 14+.
410
+ Confirm the same `roomId` is passed to `showNotification` and that Background Modes are enabled for the app target in Xcode.
134
411
 
135
- ### iOS: Notifications aren't grouping
412
+ ### iOS: Decryption failing
136
413
 
137
- Ensure `threadIdentifier` is correctly set in the push payload or that you are passing the same `roomId` to `showNotification`.
414
+ Ensure `roomDecryptedKeys` and `memberDecryptedKeys` are present in `UserDefaults` suite `"safe_storage"` before the silent push arrives.
415
+
416
+ ---
138
417
 
139
418
  ## Development
140
419
 
141
- - **Build**: `npm run build`
142
- - **Lint**: `npm run lint`
143
- - **Format**: `npm run fmt`
144
- - **Verify**: `npm run verify`
420
+ ```bash
421
+ npm run build # compile TypeScript
422
+ npm run lint # run ESLint
423
+ npm run fmt # run Prettier
424
+ npm run verify # build + lint + native checks
425
+ ```
426
+
427
+ ---
145
428
 
146
429
  ## Contributing
147
430
 
148
431
  Please see [CONTRIBUTING.md](CONTRIBUTING.md) for details.
149
432
 
433
+ ---
434
+
150
435
  ## License
151
436
 
152
437
  MIT
@@ -43,6 +43,7 @@ android {
43
43
  repositories {
44
44
  google()
45
45
  mavenCentral()
46
+ maven { url 'https://jitpack.io' }
46
47
  }
47
48
 
48
49
  dependencies {
@@ -50,12 +51,21 @@ dependencies {
50
51
  implementation project(':capacitor-android')
51
52
  implementation "androidx.appcompat:appcompat:$androidxAppCompatVersion"
52
53
  implementation "androidx.core:core:$androidxCoreVersion"
53
-
54
+
54
55
  // Socket.IO client for Android
55
56
  implementation ('io.socket:socket.io-client:2.1.0') {
56
57
  exclude group: 'org.json', module: 'json'
57
58
  }
58
-
59
+
60
+ // Libsodium JNI for E2EE decryption
61
+ implementation 'com.github.joshjdevl.libsodiumjni:libsodium-jni-aar:2.0.2'
62
+
63
+ // Google Play Services for GMS availability check
64
+ implementation 'com.google.android.gms:play-services-base:18.2.0'
65
+
66
+ // AndroidSVG for rendering avatar SVGs in notifications
67
+ implementation 'com.caverock:androidsvg:1.4'
68
+
59
69
  testImplementation 'junit:junit:4.13.2'
60
70
  androidTestImplementation 'androidx.test.ext:junit:1.1.5'
61
71
  androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1'
@@ -6,6 +6,23 @@
6
6
  android:exported="false"
7
7
  android:foregroundServiceType="dataSync" />
8
8
 
9
+ <service
10
+ android:name="com.codecraft_studio.messenger.notifications.FcmFetchForegroundService"
11
+ android:enabled="true"
12
+ android:exported="false"
13
+ android:foregroundServiceType="dataSync" />
14
+
15
+ <service
16
+ android:name="com.codecraft_studio.messenger.notifications.FcmFetchBackgroundService"
17
+ android:enabled="true"
18
+ android:exported="false" />
19
+
20
+ <service
21
+ android:name="com.codecraft_studio.messenger.notifications.FcmJobService"
22
+ android:enabled="true"
23
+ android:exported="false"
24
+ android:permission="android.permission.BIND_JOB_SERVICE" />
25
+
9
26
  <receiver
10
27
  android:name="com.codecraft_studio.messenger.notifications.NotificationDismissReceiver"
11
28
  android:enabled="true"
@@ -13,7 +30,9 @@
13
30
  </application>
14
31
 
15
32
  <uses-permission android:name="android.permission.INTERNET" />
33
+ <uses-permission android:name="android.permission.WAKE_LOCK" />
16
34
  <uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
17
35
  <uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC" />
18
36
  <uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
37
+ <uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
19
38
  </manifest>