Skip to content

Commit 1563a2b

Browse files
authored
docs(push): expand push notification documentation (W-23493430) (#2970)
docs(push): expand push notification documentation (W-23493430)
1 parent 75f091a commit 1563a2b

1 file changed

Lines changed: 247 additions & 14 deletions

File tree

docs/push/README.md

Lines changed: 247 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -2,20 +2,23 @@
22

33
This document covers the push notification subsystem in `libs/SalesforceSDK`. All classes live in the package `com.salesforce.androidsdk.push`.
44

5+
For cross-platform concepts and the shared Salesforce registration model, see the [workspace-level push doc](../../../docs/push/README.md).
6+
57
---
68

79
## Table of Contents
810

911
1. [Overview](#overview)
1012
2. [Prerequisites](#prerequisites)
11-
3. [Architecture](#architecture)
12-
4. [Key Classes](#key-classes)
13-
5. [Registration Lifecycle](#registration-lifecycle)
14-
6. [Re-registration Modes](#re-registration-modes)
15-
7. [Foreground Registration Mode](#foreground-registration-mode)
16-
8. [Encrypted Push Notifications](#encrypted-push-notifications)
17-
9. [Actionable Notifications](#actionable-notifications)
18-
10. [Testing](#testing)
13+
3. [Setup](#setup)
14+
4. [Architecture](#architecture)
15+
5. [Key Classes](#key-classes)
16+
6. [Registration Lifecycle](#registration-lifecycle)
17+
7. [Re-registration Modes](#re-registration-modes)
18+
8. [Foreground Registration Mode](#foreground-registration-mode)
19+
9. [Encrypted Push Notifications](#encrypted-push-notifications)
20+
10. [Actionable Notifications](#actionable-notifications)
21+
11. [Testing](#testing)
1922

2023
---
2124

@@ -32,10 +35,74 @@ The SDK integrates Firebase Cloud Messaging (FCM) to deliver push notifications
3235
| `google-services.json` | Place in your app module root; required for FCM token acquisition |
3336
| `com.google.gms:google-services` plugin | Apply in your app-level `build.gradle.kts` |
3437
| `PushNotificationInterface` implementation | Register via `SalesforceSDKManager.getInstance().pushNotificationReceiver` |
38+
| `POST_NOTIFICATIONS` permission | Required for Android 13+ (API 33+); declare in manifest and request at runtime |
3539
| External Client App | Connected app with push notification endpoint enabled in your Salesforce org |
3640

3741
---
3842

43+
## Setup
44+
45+
### 1. Add google-services.json
46+
47+
Download `google-services.json` from the [Firebase Console](https://console.firebase.google.com/) and place it in your app module directory (e.g. `app/`).
48+
49+
### 2. Apply the Google Services Gradle Plugin
50+
51+
**`build.gradle.kts`** (project level):
52+
```kotlin
53+
buildscript {
54+
dependencies {
55+
classpath("com.google.gms:google-services:4.4.2")
56+
}
57+
}
58+
```
59+
60+
**`app/build.gradle.kts`** (app level):
61+
```kotlin
62+
plugins {
63+
id("com.google.gms.google-services")
64+
}
65+
```
66+
67+
### 3. Declare POST_NOTIFICATIONS Permission (Android 13+)
68+
69+
**`AndroidManifest.xml`**:
70+
```xml
71+
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
72+
```
73+
74+
Request the permission at runtime in your `Activity` or `MainActivity`:
75+
```kotlin
76+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU &&
77+
checkSelfPermission(android.Manifest.permission.POST_NOTIFICATIONS)
78+
!= PackageManager.PERMISSION_GRANTED) {
79+
requestPermissions(arrayOf(android.Manifest.permission.POST_NOTIFICATIONS), requestCode)
80+
}
81+
```
82+
83+
### 4. Implement PushNotificationInterface
84+
85+
In your `Application.onCreate()` after `SalesforceSDKManager` is initialized:
86+
87+
```kotlin
88+
// Native Android apps
89+
SalesforceSDKManager.getInstance().pushNotificationReceiver =
90+
object : PushNotificationInterface {
91+
override fun onPushMessageReceived(data: Map<String?, String?>?) {
92+
// Handle the notification. The SDK has already decrypted any encrypted payload.
93+
}
94+
override fun supplyFirebaseMessaging(): FirebaseMessaging? = null
95+
}
96+
97+
// React Native apps — use SalesforceReactSDKManager instead
98+
SalesforceReactSDKManager.getInstance().pushNotificationReceiver =
99+
object : PushNotificationInterface { /* ... */ }
100+
```
101+
102+
That is all. `SFDCFcmListenerService` (declared in the SDK's own `AndroidManifest.xml`) handles FCM token acquisition, Salesforce device registration, decryption, and re-registration automatically.
103+
104+
---
105+
39106
## Architecture
40107

41108
```
@@ -183,14 +250,89 @@ Encryption scheme: RSA-OAEP-SHA256 wrapping a 128-bit AES key + 128-bit IV.
183250
Serializable data class modelling the Salesforce actionable notification payload under the `sfdc` key.
184251

185252
```kotlin
186-
data class SalesforceActionableNotificationContent(val sfdc: Sfdc?)
253+
@Serializable
254+
data class SalesforceActionableNotificationContent(val sfdc: Sfdc?) {
255+
companion object {
256+
fun fromJson(json: String): SalesforceActionableNotificationContent
257+
}
258+
259+
@Serializable
260+
data class Sfdc(
261+
val notifType: String? = null,
262+
val nid: String? = null, // notification ID (use for invokeServerNotificationAction)
263+
val oid: String? = null, // org ID
264+
val type: Int? = null,
265+
val alertTitle: String? = null,
266+
val alertBody: String? = null,
267+
val alert: String? = null,
268+
val sid: String? = null,
269+
val rid: String? = null,
270+
val targetPageRef: String? = null,
271+
val badge: Int? = null,
272+
val uid: String? = null,
273+
val cid: String? = null,
274+
val timestamp: Int? = null,
275+
val act: Act? = null // actionable action descriptor
276+
)
277+
}
187278
```
188279

189-
Parse from the `content` field of a received notification:
280+
Parse from the `"content"` key of the received `data` map (the full JSON wraps the `sfdc` object):
190281
```kotlin
191-
val content = SalesforceActionableNotificationContent.fromJson(jsonString)
282+
override fun onPushMessageReceived(data: Map<String?, String?>?) {
283+
val sfdc = data?.get("content")
284+
?.let { SalesforceActionableNotificationContent.fromJson(it) }
285+
?.sfdc ?: return
286+
val notificationId = sfdc.nid
287+
// use notificationId with SalesforceSDKManager.invokeServerNotificationAction(...)
288+
}
192289
```
193290

291+
**Note on `Act`:** `sfdc.act?.group` is a string identifier that matches `notificationType.actionGroups[].name` — the action *buttons* (with `label` and `actionKey`) live on the notification type fetched from the API, not inside the `act` field of the incoming payload.
292+
293+
---
294+
295+
### `NotificationsApiClient`
296+
297+
REST client for the Salesforce Notifications API (requires API v64.0+).
298+
299+
```kotlin
300+
class NotificationsApiClient(private val restClient: RestClient) {
301+
fun fetchNotificationsTypes(): NotificationsTypesResponseBody?
302+
fun submitNotificationAction(notificationId: String, actionKey: String): NotificationsActionsResponseBody?
303+
}
304+
```
305+
306+
Use via `SalesforceSDKManager`:
307+
```kotlin
308+
// Fetch stored notification types (populated automatically after registration)
309+
val type = SalesforceSDKManager.getInstance().getNotificationsType("approval_request")
310+
311+
// Invoke a server-side notification action
312+
SalesforceSDKManager.getInstance().invokeServerNotificationAction(
313+
notificationId = nid,
314+
actionKey = actionIdentifier,
315+
restClient = restClient
316+
)
317+
```
318+
319+
---
320+
321+
### React Native Apps
322+
323+
For React Native apps, the setup is identical to the native Android setup above, with one substitution: use `SalesforceReactSDKManager.getInstance()` in place of `SalesforceSDKManager.getInstance()`:
324+
325+
```kotlin
326+
// In MainApplication.onCreate(), after SalesforceReactSDKManager.initReactNative(...)
327+
SalesforceReactSDKManager.getInstance().pushNotificationReceiver =
328+
object : PushNotificationInterface {
329+
override fun onPushMessageReceived(data: Map<String?, String?>?) { /* ... */ }
330+
override fun supplyFirebaseMessaging(): FirebaseMessaging? = null
331+
}
332+
```
333+
334+
There is no JavaScript push bridge — the `react-native-force` package does not export a push module. See [`SalesforceMobileSDK-ReactNative/docs/push/README.md`](https://github.com/forcedotcom/SalesforceMobileSDK-ReactNative/tree/dev/docs/push) for guidance on forwarding push payloads to the JavaScript layer via a custom event emitter.
335+
194336
---
195337

196338
## Registration Lifecycle
@@ -282,14 +424,105 @@ No app-side configuration is required beyond normal SDK setup.
282424

283425
---
284426

427+
## Actionable Notifications
428+
429+
Requires Salesforce API version **v64.0 or later**.
430+
431+
After successful device registration, the SDK automatically fetches notification types from `GET /vXX.0/connect/notifications/types` and stores them per user. It creates one `NotificationChannel` per Salesforce notification type (channel ID = `notificationType.type`, importance `HIGH`), grouped under a `NotificationChannelGroup` named `"Salesforce Notifications"`.
432+
433+
Actionable notifications require three collaborating pieces:
434+
435+
1. **Payload parsing** — parse `"content"` in `onPushMessageReceived`, look up the notification type by `notifType`.
436+
2. **Notification building** — construct a `NotificationCompat` with action buttons sourced from the notification type's `actionGroups`.
437+
3. **Action handling** — a `BroadcastReceiver` receives the `PendingIntent` when the user taps a button and calls `invokeServerNotificationAction`.
438+
439+
### 1. Parse payload and build the notification
440+
441+
```kotlin
442+
override fun onPushMessageReceived(data: Map<String?, String?>?) {
443+
// Payload key is "content" — the SDK has already decrypted it
444+
val sfdc = data?.get("content")
445+
?.let { SalesforceActionableNotificationContent.fromJson(it) }
446+
?.sfdc ?: return
447+
448+
// Look up the stored notification type (fetched automatically after registration)
449+
val notifType = sfdc.notifType?.let {
450+
SalesforceSDKManager.getInstance().getNotificationsType(it)
451+
} ?: return
452+
453+
// Build notification using notifType.type as the channel ID
454+
val notification = NotificationCompat.Builder(context, notifType.type).apply {
455+
setContentTitle(sfdc.alertTitle)
456+
setContentText(sfdc.alert)
457+
setStyle(NotificationCompat.BigTextStyle().bigText(sfdc.alertBody))
458+
459+
// Action buttons come from the notification type's actionGroups,
460+
// matched by sfdc.act?.group (the payload carries a group name, not the buttons)
461+
notifType.actionGroups
462+
?.firstOrNull { it.name == sfdc.act?.group }
463+
?.actions
464+
?.forEach { action ->
465+
addAction(
466+
android.R.drawable.ic_menu_send,
467+
action.label,
468+
PendingIntent.getBroadcast(
469+
context, 0,
470+
Intent("com.example.PUSH_ACTION").apply {
471+
putExtra("notificationId", sfdc.nid)
472+
putExtra("actionKey", action.actionKey)
473+
},
474+
PendingIntent.FLAG_IMMUTABLE
475+
)
476+
)
477+
}
478+
}.build()
479+
context.getSystemService(NotificationManager::class.java)
480+
.notify(sfdc.nid.hashCode(), notification)
481+
}
482+
```
483+
484+
### 2. Handle the action tap with a BroadcastReceiver
485+
486+
```kotlin
487+
class PushActionReceiver : BroadcastReceiver() {
488+
override fun onReceive(context: Context, intent: Intent) {
489+
val nid = intent.getStringExtra("notificationId") ?: return
490+
val actionKey = intent.getStringExtra("actionKey") ?: return
491+
CoroutineScope(Dispatchers.IO).launch {
492+
try {
493+
// restClient defaults to clientManager.peekRestClient(currentUser)
494+
SalesforceSDKManager.getInstance()
495+
.invokeServerNotificationAction(notificationId = nid, actionKey = actionKey)
496+
} catch (e: Exception) {
497+
Log.e("Push", "Action invocation failed", e)
498+
}
499+
}
500+
}
501+
}
502+
```
503+
504+
Register the receiver in `Application.onCreate()` (not in the manifest — use `RECEIVER_NOT_EXPORTED` to prevent external apps from sending intents to it):
505+
506+
```kotlin
507+
ContextCompat.registerReceiver(
508+
this,
509+
PushActionReceiver(),
510+
IntentFilter("com.example.PUSH_ACTION"),
511+
ContextCompat.RECEIVER_NOT_EXPORTED
512+
)
513+
```
514+
515+
---
516+
285517
## Testing
286518

287-
Push notification tests are in `libs/test/SalesforceSDKTest/src/com/salesforce/androidsdk/app/`:
519+
Push notification tests are in `libs/test/SalesforceSDKTest/src/com/salesforce/androidsdk/`:
288520

289521
| Test class | Coverage |
290522
|---|---|
291-
| `PushServiceTest` | SFDC registration/unregistration endpoint communication, notification channel creation/deletion, status callbacks, HTTP error handling |
292-
| `PushMessagingTest` | Notification type storage/retrieval, `SalesforceSDKManager` notification API delegation, `NotificationsApiClient` content-type behaviour |
523+
| `push/PushServiceTest` | SFDC registration/unregistration endpoint communication, notification channel creation/deletion, status callbacks, HTTP error handling |
524+
| `push/PushMessagingTest` | Notification type storage/retrieval, `SalesforceSDKManager` notification API delegation, `NotificationsApiClient` content-type behaviour |
525+
| `push/PushNotificationsRegistrationChangeWorkerTest` | WorkManager worker account-resolution logic, legacy pre-14.0 payload migration |
293526

294527
Tests use [MockK](https://mockk.io/) to mock `RestClient` and `RestResponse`. They run as instrumented tests on-device or on Firebase Test Lab:
295528

0 commit comments

Comments
 (0)