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.
- package/MessengerNotifications.podspec +2 -1
- package/Package.swift +4 -2
- package/README.md +344 -59
- package/android/build.gradle +12 -2
- package/android/src/main/AndroidManifest.xml +19 -0
- package/android/src/main/java/com/codecraft_studio/messenger/notifications/EncryptedMessageNotifier.java +689 -40
- package/android/src/main/java/com/codecraft_studio/messenger/notifications/FcmFetchBackgroundService.java +31 -0
- package/android/src/main/java/com/codecraft_studio/messenger/notifications/FcmFetchForegroundService.java +177 -0
- package/android/src/main/java/com/codecraft_studio/messenger/notifications/FcmFetchManager.java +245 -3
- package/android/src/main/java/com/codecraft_studio/messenger/notifications/FcmJobService.java +62 -0
- package/android/src/main/java/com/codecraft_studio/messenger/notifications/FcmTokenRegistrar.java +142 -0
- package/android/src/main/java/com/codecraft_studio/messenger/notifications/GmsHelper.java +20 -0
- package/android/src/main/java/com/codecraft_studio/messenger/notifications/MessageFlowLogger.java +151 -0
- package/android/src/main/java/com/codecraft_studio/messenger/notifications/MessengerNotificationsPlugin.java +206 -15
- package/android/src/main/java/com/codecraft_studio/messenger/notifications/NativeCrypto.java +129 -27
- package/android/src/main/java/com/codecraft_studio/messenger/notifications/NotificationDismissReceiver.java +4 -5
- package/android/src/main/java/com/codecraft_studio/messenger/notifications/NotificationHelper.java +420 -56
- package/android/src/main/java/com/codecraft_studio/messenger/notifications/PersistentSocketService.java +116 -30
- package/android/src/main/java/com/codecraft_studio/messenger/notifications/TemporarySocketSessionManager.java +528 -0
- package/android/src/main/java/com/codecraft_studio/messenger/notifications/UnreadMessagesFetcher.java +220 -0
- package/android/src/main/res/drawable/ic_notification.png +0 -0
- package/android/src/main/res/drawable/ic_transparent.png +0 -0
- package/android/src/main/res/values/strings.xml +4 -0
- package/dist/esm/definitions.d.ts +7 -0
- package/dist/esm/definitions.js.map +1 -1
- package/dist/esm/web.d.ts +3 -0
- package/dist/esm/web.js +3 -0
- package/dist/esm/web.js.map +1 -1
- package/dist/plugin.cjs.js +3 -0
- package/dist/plugin.cjs.js.map +1 -1
- package/dist/plugin.js +3 -0
- package/dist/plugin.js.map +1 -1
- package/ios/Sources/MessengerNotificationsPlugin/EncryptedMessageNotifier.swift +362 -0
- package/ios/Sources/MessengerNotificationsPlugin/FcmTokenRegistrar.swift +228 -0
- package/ios/Sources/MessengerNotificationsPlugin/MessengerNotificationsPlugin.swift +45 -16
- package/ios/Sources/MessengerNotificationsPlugin/NativeCrypto.swift +73 -14
- package/ios/Sources/MessengerNotificationsPlugin/NotificationHelper.swift +777 -40
- package/ios/Sources/MessengerNotificationsPlugin/SafeStorageStore.swift +229 -5
- package/ios/Sources/MessengerNotificationsPlugin/TemporarySocketSessionManager.swift +1955 -57
- package/ios/Sources/MessengerNotificationsPlugin/UnreadMessagesFetcher.swift +544 -0
- 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', '~>
|
|
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
|
-
- [
|
|
14
|
-
- [
|
|
15
|
-
- [
|
|
16
|
-
- [
|
|
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**:
|
|
27
|
-
- **
|
|
28
|
-
- **
|
|
29
|
-
- **
|
|
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
|
-
###
|
|
71
|
+
### Permissions
|
|
48
72
|
|
|
49
|
-
|
|
73
|
+
Before showing notifications, check and request permission:
|
|
50
74
|
|
|
51
75
|
```typescript
|
|
52
76
|
import { MessengerNotifications } from 'capacitor-messenger-notifications';
|
|
53
77
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
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
|
-
|
|
88
|
+
### Show a Notification
|
|
62
89
|
|
|
63
90
|
```typescript
|
|
64
91
|
await MessengerNotifications.showNotification({
|
|
65
|
-
title: '
|
|
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
|
-
###
|
|
103
|
+
### Clear a Room's Notifications
|
|
75
104
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
- **iOS**: Use `TemporarySocketSessionManager.shared.fetchAndNotify(...)` inside `didReceiveRemoteNotification`.
|
|
105
|
+
```typescript
|
|
106
|
+
await MessengerNotifications.clearRoomNotification({ roomId: 101 });
|
|
107
|
+
```
|
|
80
108
|
|
|
81
|
-
|
|
109
|
+
### Cold-Start Navigation
|
|
82
110
|
|
|
83
|
-
|
|
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
|
-
```
|
|
86
|
-
{
|
|
87
|
-
|
|
88
|
-
|
|
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
|
-
|
|
120
|
+
### Persistent Socket (Android)
|
|
98
121
|
|
|
99
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
111
|
-
|
|
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
|
-
|
|
221
|
+
#### 1. Capabilities
|
|
116
222
|
|
|
117
|
-
|
|
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
|
-
|
|
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
|
-
| `
|
|
122
|
-
| `
|
|
123
|
-
| `
|
|
124
|
-
| `
|
|
125
|
-
| `
|
|
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:
|
|
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
|
-
|
|
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:
|
|
412
|
+
### iOS: Decryption failing
|
|
136
413
|
|
|
137
|
-
Ensure `
|
|
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
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
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
|
package/android/build.gradle
CHANGED
|
@@ -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>
|