Push notifications
Notify your users when other members interact with them in the community, open the right post or comment when they tap the notification, and show how many notifications they have not seen yet.
Android availableiOS availableFlutter availableReact Native availableUnity available
Before you begin
- Complete the Quickstart: the SDK is initialized and the community screen opens in your app.
- Set up push notifications in your app: Firebase Cloud Messaging for Android builds, APNs for iOS builds.
- Send Octopus the credentials that let its servers push to your app, through the secure form the Octopus team gives you:
| Build | What to send | Where to get it |
|---|---|---|
| Android | The private key file (JSON) of a Firebase service account | Firebase console, Settings > Service Accounts, Generate New Private Key. See Provide credentials manually. |
| iOS | An APNs authentication key (.p8), its key ID, your Bundle ID and your Team ID | Keys page of the Apple Developer site: create a key with Apple Push Notifications service (APNs) enabled and the Sandbox & Production environment. The key ID is in the list of keys, the Bundle ID in your Xcode project, the Team ID in the Membership details card. |
The table applies to every platform: a Flutter, React Native or Unity app sends the Android credentials for its Android build and the iOS credentials for its iOS build.
The service account key is not the google-services.json file that configures Firebase in your app. Octopus needs the service account key.
How it works
The SDK never asks for the notification permission: your app requests it where it makes sense in your flow. The SDK registers the token, recognizes its own notifications and opens the community on their content.
Your app ── (1) push token ───────────────────▶ Octopus backend
FCM token on Android │
APNs token on iOS │
(2) someone interacts with the user
│
┌────────────────────────────────────┴───────────────┐
▼ ▼
FCM, data-only message APNs message
│ │
▼ ▼
(3) Android: your app builds (3) iOS: the system shows
and shows the notification the notification
└────────────────────────┬───────────────────────────┘
▼
(4) the user taps it
│
▼
(5) your app checks that it is an Octopus notification
and hands it to the SDK, which opens the post or comment
| Step | Android | iOS |
|---|---|---|
| Token | FCM registration token | APNs device token |
| Display | Your app. Octopus sends data-only FCM messages, which neither the system nor Firebase displays, even in the background. | The system displays the APNs notification. |
| Tap | Your app routes the tap to the SDK. | Your app routes the tap to the SDK. |
1. Register the push token
Register the token after the SDK is initialized, then again each time it changes. In an iOS app, whatever the SDK, always register the APNs device token. Octopus sends iOS pushes through APNs with its own key, so an FCM token registered on iOS gives a successful call and no notification.
- Android
- iOS
- Flutter
- React Native
- Unity
Declare the permission and your messaging service in AndroidManifest.xml:
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<application>
<service
android:name=".notifications.MessagingService"
android:exported="false">
<intent-filter>
<action android:name="com.google.firebase.MESSAGING_EVENT" />
</intent-filter>
</service>
</application>
Forward every new FCM token from the service:
class MessagingService : FirebaseMessagingService() {
override fun onNewToken(token: String) {
super.onNewToken(token)
OctopusSDK.registerNotificationsToken(token)
}
}
onNewToken only runs when Firebase issues a token. Register the current one at startup too, so a token issued before you integrated Octopus is registered, and request the permission on Android 13 and above:
class MainActivity : ComponentActivity() {
private val requestNotificationPermission =
registerForActivityResult(ActivityResultContracts.RequestPermission()) { }
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU &&
checkSelfPermission(Manifest.permission.POST_NOTIFICATIONS) != PackageManager.PERMISSION_GRANTED
) {
requestNotificationPermission.launch(Manifest.permission.POST_NOTIFICATIONS)
}
FirebaseMessaging.getInstance().token.addOnSuccessListener { token ->
OctopusSDK.registerNotificationsToken(token)
}
}
}
Request the authorization and register for remote notifications:
let center = UNUserNotificationCenter.current()
try await center.requestAuthorization(options: [.alert, .badge, .sound])
UIApplication.shared.registerForRemoteNotifications()
Forward the device token, as a hexadecimal string, from your app delegate:
func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) {
let token = deviceToken.map { String(format: "%02.2hhx", $0) }.joined()
octopus.set(notificationDeviceToken: token)
}
With firebase_messaging, pass getAPNSToken() on iOS and getToken() on Android. On a refresh, fetch the token again rather than using the callback value, which is the FCM token on both platforms:
import 'dart:io' show Platform;
import 'package:firebase_messaging/firebase_messaging.dart';
import 'package:octopus_sdk_flutter/octopus_sdk_flutter.dart';
Future<void> setUpOctopusPushToken() async {
final messaging = FirebaseMessaging.instance;
await messaging.requestPermission();
await _registerToken(messaging);
messaging.onTokenRefresh.listen((_) => _registerToken(messaging));
}
Future<void> _registerToken(FirebaseMessaging messaging) async {
final token = Platform.isIOS
? await messaging.getAPNSToken()
: await messaging.getToken();
if (token == null || token.isEmpty) return;
await OctopusSDK().registerPushNotificationToken(token);
}
Call setUpOctopusPushToken() once the SDK is initialized: a token registered before is dropped.
Pass the FCM token on Android and the APNs device token, as a hexadecimal string, on iOS. With @react-native-firebase/messaging, that is getToken() on Android and getAPNSToken() on iOS. The user does not need to be connected: Octopus links the token to the user when connectUser() runs, and again when the user changes.
import { Platform } from 'react-native';
import messaging from '@react-native-firebase/messaging';
import { registerPushNotificationToken } from '@octopus-community/react-native';
async function registerToken() {
const token = Platform.OS === 'ios'
? await messaging().getAPNSToken()
: await messaging().getToken();
if (token) {
await registerPushNotificationToken(token);
}
}
export async function setUpOctopusPushToken() {
await messaging().requestPermission();
await registerToken();
messaging().onTokenRefresh(() => {
registerToken();
});
}
Firebase is not needed on iOS. Without it, get the token from a native module that registers for remote notifications. The SDK repository's example app ships one in example/ios/OctopusReactNativeSdkExample/OctopusPushModule.swift, with its delegate callbacks in AppDelegate.swift.
On iOS, use the Mobile Notifications package (com.unity.mobile.notifications). It requests the authorization and returns the APNs token; iOS needs no Firebase and no native file:
using System.Collections;
using Unity.Notifications.iOS;
IEnumerator RequestIOSAuthorization()
{
using (var req = new AuthorizationRequest(
AuthorizationOption.Alert | AuthorizationOption.Sound | AuthorizationOption.Badge,
registerForRemoteNotifications: true))
{
while (!req.IsFinished) yield return null;
if (req.Granted && !string.IsNullOrEmpty(req.DeviceToken))
OctopusSDK.RegisterNotificationsToken(req.DeviceToken);
}
}
On Android, add the google-services.json of your Firebase project to Assets/, then register the current token at launch and every refreshed one with the Firebase Unity SDK:
Firebase.FirebaseApp.CheckAndFixDependenciesAsync().ContinueWithOnMainThread(task => {
if (task.Result != Firebase.DependencyStatus.Available) return;
Firebase.Messaging.FirebaseMessaging.TokenReceived += (sender, e) =>
OctopusSDK.RegisterNotificationsToken(e.Token);
Firebase.Messaging.FirebaseMessaging.GetTokenAsync().ContinueWithOnMainThread(tokenTask => {
if (!tokenTask.IsFaulted && !tokenTask.IsCanceled && !string.IsNullOrEmpty(tokenTask.Result))
OctopusSDK.RegisterNotificationsToken(tokenTask.Result);
});
});
2. Display the notification
In an iOS app, the system displays the notification. In an Android app, whatever the SDK, your app builds and posts it from the data-only FCM message, in the foreground and in the background.
- Android
- iOS
- Flutter
- React Native
- Unity
In your messaging service, parse the message with getOctopusNotification, which returns null for a message that is not from Octopus. setOctopusContent sets the title, the text and a tap intent that deep-links into the Octopus screens:
override fun onMessageReceived(remoteMessage: RemoteMessage) {
super.onMessageReceived(remoteMessage)
val octopusNotification = OctopusSDK.getOctopusNotification(data = remoteMessage.data)
if (octopusNotification != null) {
notificationManager.notify(
octopusNotification.id,
NotificationCompat.Builder(this, CHANNEL_ID)
.setSmallIcon(R.drawable.ic_stat_notification)
.setAutoCancel(true)
.setOctopusContent(
context = this,
activityClass = MainActivity::class,
octopusNotification = octopusNotification
)
.build()
)
}
}
activityClass is the activity whose Compose content holds the octopusComposables() graph. To build the tap intent yourself, use the title, body and linkPath of the OctopusNotification.
Nothing to write: the system displays the notification that APNs delivers. To show it while your app is in the foreground, implement userNotificationCenter(_:willPresent:withCompletionHandler:) as for any other notification.
On iOS, the system displays the notification. On Android, firebase_messaging receives the message and nothing is shown, so display it yourself, for example with flutter_local_notifications. Keep the Octopus keys in the notification payload so the tap can open the right content:
import 'dart:convert';
import 'package:firebase_messaging/firebase_messaging.dart';
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
import 'package:octopus_sdk_flutter/octopus_sdk_flutter.dart';
final localNotifications = FlutterLocalNotificationsPlugin();
Future<void> showOctopusPushNotification(Map<String, Object?> payload) async {
if (!OctopusSDK.isOctopusNotification(payload)) return;
final n = OctopusSDK.getOctopusNotification(payload);
if (n == null) return;
await localNotifications.show(
n.linkPath.hashCode,
n.title,
n.body,
const NotificationDetails(
android: AndroidNotificationDetails('octopus-sdk', 'Community'),
),
payload: jsonEncode(n.rawPayload),
);
}
// Background and terminated app: top-level handler.
@pragma('vm:entry-point')
Future<void> octopusFcmBackgroundHandler(RemoteMessage message) async {
// Initialize localNotifications in this isolate first.
await showOctopusPushNotification(message.data);
}
// Call at startup, on Android.
void listenForOctopusPushes() {
FirebaseMessaging.onBackgroundMessage(octopusFcmBackgroundHandler);
FirebaseMessaging.onMessage.listen((m) => showOctopusPushNotification(m.data));
}
The background handler runs in its own isolate: initialize flutter_local_notifications and create the channel there too. The example app of the SDK repository shows the full setup.
On iOS, the system displays the notification. On Android, display it yourself, for example with Notifee. The parsing functions are pure JavaScript, so they also work in the background handler, before initialize():
import messaging from '@react-native-firebase/messaging';
import notifee, { AndroidImportance } from '@notifee/react-native';
import {
isOctopusNotification,
getOctopusNotification,
} from '@octopus-community/react-native';
export async function displayOctopusNotification(data: Record<string, any>) {
if (!isOctopusNotification(data)) return;
const n = getOctopusNotification(data);
if (!n) return;
await notifee.createChannel({ id: 'octopus-sdk', name: 'Community', importance: AndroidImportance.DEFAULT });
await notifee.displayNotification({
title: n.title,
body: n.body,
data: n.rawPayload,
android: { channelId: 'octopus-sdk', smallIcon: 'ic_launcher', pressAction: { id: 'default' } },
});
}
// In index.js, before AppRegistry, on Android only:
messaging().setBackgroundMessageHandler(async (m) => displayOctopusNotification({ ...(m.data ?? {}) }));
// Once the app runs:
messaging().onMessage((m) => displayOctopusNotification({ ...(m.data ?? {}) }));
On iOS, the system displays the notification. On Android, C# is paused while the app is in the background, so a native FirebaseMessagingService must post the notification. The Push Notifications Example sample ships that service. Import it from the Package Manager and keep its two Android files, which land in your project so you can edit them:
| File | Role |
|---|---|
Plugins/Android/OctopusMessagingService.java | Extends com.google.firebase.messaging.cpp.ListenerService, the messaging service of the Firebase Unity plugin. Posts each message that carries the is_octopus_notification marker on an octopus-sdk channel, with the app icon and the payload's title and body, and puts the data map in the tap intent. It then calls super, so FirebaseMessaging.MessageReceived and TokenReceived keep firing in C#. |
Plugins/Android/OctopusPushSample.androidlib/ | Registers that service on com.google.firebase.MESSAGING_EVENT with android:priority="1", above the plugin's own service, so FCM binds it. |
For production, ship a monochrome drawable as the small icon: an adaptive launcher icon renders as a grey square in the status bar.
The service is verified against Firebase Unity SDK 13.8.0. It extends an internal class of that plugin: if a later version renames or removes it, the Android build fails on that symbol. Pin the plugin version, or extend com.google.firebase.messaging.FirebaseMessagingService instead and lose the MessageReceived and TokenReceived events in C#.
3. Handle notification taps
When the user taps a notification, check that it comes from Octopus, then hand it to the SDK. The SDK opens the community on the post, comment or reply the notification is about. No platform opens it in a modal bottom sheet; where it opens is set per platform below.
- Android
- iOS
- Flutter
- React Native
- Unity
The destination opens in your own navigation graph, so you choose its container. The tap intent built by setOctopusContent launches activityClass with the notification's deep link as its data. Navigate to it:
intent.data?.let { uri -> navController.navigate(deepLink = uri) }
You present OctopusHomeScreen the way you want: in a .sheet, a .fullScreenCover or pushed on your own navigation stack. When the user taps a notification, read its userInfo and check that it is from Octopus:
let userInfo = notificationResponse.notification.request.content.userInfo
if OctopusSDK.isAnOctopusNotification(userInfo: userInfo) {
octopusNotificationUserInfo = userInfo
// Present the Octopus UI
}
Pass the stored userInfo to OctopusHomeScreen as a binding:
OctopusHomeScreen(octopus: octopus, notificationUserInfo: $octopusNotificationUserInfo)
The binding is a [AnyHashable: Any]?. The SDK sets it back to nil once it has navigated to the notification's screen.
openNotification pushes a full-screen route (MaterialPageRoute) that hosts the community, opened on the notification's content. To present it differently, for example as a fullscreenDialog, build the route yourself from the example app instead of calling the helper.
Future<void> openOctopusNotification(BuildContext context, Map payload) async {
if (!OctopusSDK.isOctopusNotification(payload)) return;
final notification = OctopusSDK.getOctopusNotification(payload);
if (notification == null) return;
await OctopusSDK().openNotification(
context,
notification,
onNavigateToLogin: () {
// Open your app's login screen
},
);
}
onNavigateToLogin is required. The optional parameters are navBarTitle, navBarPrimaryColor, theme, onModifyUser, onNavigateToUrl, onNavigateToProfile, bottomSafeAreaInset, onBack and navBarLeadingAction, as on showOctopusHomeScreen (see Customize the top app bar). onBack is called right before the helper pops the screen it opened.
getOctopusNotification returns null when the payload has no link_path. The returned OctopusNotification has title, body, linkPath, rawPayload and the optional postId, commentId and replyId.
Route every tap to that function, with a navigatorKey on your MaterialApp to get a context:
Future<void> setUpOctopusTaps() async {
// Android: tap on a notification displayed with flutter_local_notifications.
await localNotifications.initialize(
const InitializationSettings(
android: AndroidInitializationSettings('ic_stat_notification'),
iOS: DarwinInitializationSettings(),
),
onDidReceiveNotificationResponse: (response) {
final raw = response.payload;
final ctx = navigatorKey.currentContext;
if (raw == null || ctx == null) return;
openOctopusNotification(ctx, jsonDecode(raw) as Map);
},
);
// Android cold start from that notification: read
// localNotifications.getNotificationAppLaunchDetails() the same way.
// iOS: tap on the system notification, app in the background or terminated.
FirebaseMessaging.onMessageOpenedApp.listen((m) {
final ctx = navigatorKey.currentContext;
if (ctx != null) openOctopusNotification(ctx, m.data);
});
final initial = await FirebaseMessaging.instance.getInitialMessage();
final launchContext = navigatorKey.currentContext;
if (initial != null && launchContext != null) {
openOctopusNotification(launchContext, initial.data);
}
}
openNotification presents the same full-screen container as openUI, opened on the notification's content. Its optional second argument takes onBackRequested, with the same contract as on openUI.
getOctopusNotification returns null when the payload has no link_path. The returned object has title, body, linkPath, rawPayload and the optional postId, commentId and replyId. isOctopusNotification and getOctopusNotification accept the Android FCM data map and the iOS userInfo as they are.
import messaging from '@react-native-firebase/messaging';
import notifee, { EventType } from '@notifee/react-native';
import {
isOctopusNotification,
getOctopusNotification,
openNotification,
} from '@octopus-community/react-native';
function routeTap(data?: Record<string, any> | null) {
if (!data || !isOctopusNotification(data)) return;
const notification = getOctopusNotification(data);
if (notification) {
openNotification(notification);
}
}
export async function setUpOctopusTaps() {
// Android: taps on the notifications displayed with Notifee.
notifee.onForegroundEvent(({ type, detail }) => {
if (type === EventType.PRESS) routeTap(detail.notification?.data);
});
const initialNotifee = await notifee.getInitialNotification();
if (initialNotifee) routeTap(initialNotifee.notification.data);
// Register notifee.onBackgroundEvent the same way in index.js.
// iOS with Firebase: taps on the system notification.
messaging().onNotificationOpenedApp((m) => routeTap(m.data));
const initialFcm = await messaging().getInitialNotification();
if (initialFcm) routeTap(initialFcm.data);
}
Without Firebase on iOS, the native module of the example app also forwards the tapped userInfo to JavaScript; pass it to routeTap.
GetOctopusNotification always returns an object, even for a payload that is not from Octopus: check IsOctopusNotification first.
OctopusSDK.Open(notification) shows the native Octopus UI full screen over the game, on the notification's content. The game resumes when the user closes it.
void HandleTappedPayload(IDictionary<string, string> payload)
{
if (!OctopusSDK.IsOctopusNotification(payload)) return;
var notification = OctopusSDK.GetOctopusNotification(payload);
OctopusSDK.Open(notification);
}
The payload is the FCM data map on Android and the notification's UserInfo on iOS; both shapes work. Read the tap from the scene that loads first, or from a DontDestroyOnLoad object, so a tap that launches the app is handled at once.
On iOS, iOSNotificationCenter.GetLastRespondedNotification() returns the tapped notification. Check it in Start() for a cold start and in OnApplicationFocus(true) for a resume, and skip a deep link you already handled:
using System.Collections.Generic;
using Unity.Notifications.iOS;
using UnityEngine;
public class PushHandler : MonoBehaviour
{
string _lastHandledDeepLink;
void Start()
{
StartCoroutine(RequestIOSAuthorization());
HandleRespondedNotification();
}
void OnApplicationFocus(bool hasFocus)
{
if (hasFocus) HandleRespondedNotification();
}
void HandleRespondedNotification()
{
var responded = iOSNotificationCenter.GetLastRespondedNotification();
if (responded == null) return;
var payload = responded.UserInfo;
if (!OctopusSDK.IsOctopusNotification(payload)) return;
var notification = OctopusSDK.GetOctopusNotification(payload);
if (notification.DeepLink == _lastHandledDeepLink) return;
_lastHandledDeepLink = notification.DeepLink;
OctopusSDK.Open(notification);
}
}
iOSNotificationCenter.OnRemoteNotificationReceived fires when a notification arrives while the game is in the foreground, not on a tap. Subscribe to it only if you want to react to that arrival.
On Android, a tap launches or resumes your activity with the data map in the intent extras. Read them in Start() and OnApplicationFocus(true), then pass them to HandleTappedPayload. HandleAndroidLaunchIntent() in the sample's PushNotificationsExample.cs does it with AndroidJavaObject. A message that arrives in the foreground also raises Firebase.Messaging.FirebaseMessaging.MessageReceived in C#; OctopusSDK.GetOctopusNotification(e.Message.Data) gives its title and body.
4. Show the not-seen count
Show a badge with the number of notifications the user has not seen in the community, to bring them back to what is new. The SDK publishes the count and updates it when it changes; ask for a refresh from the server when you need the latest value, for example when your app comes to the foreground.
- Android
- iOS
- Flutter
- React Native
- Unity
Collect OctopusSDK.notSeenNotificationsCount, a Flow<Int>. updateNotSeenNotificationsCount() is a suspend function that fetches the count and returns it as an OctopusResult<Int, Nothing>:
lifecycleScope.launch {
OctopusSDK.notSeenNotificationsCount.collect { count ->
// Update your badge
}
}
lifecycleScope.launch {
OctopusSDK.updateNotSeenNotificationsCount()
}
The Android sample app shows a full example.
Observe notSeenNotificationsCount, a @Published Int of your OctopusSDK instance, and refresh it with:
try await octopus.updateNotSeenNotificationsCount()
The Notification Badge scenario of the iOS sample app shows a full example.
Listen to OctopusSDK.notSeenNotificationsCount, a Stream<int>, and refresh it with updateNotSeenNotificationsCount():
OctopusSDK.notSeenNotificationsCount.listen((count) {
// Update your badge
});
await OctopusSDK().updateNotSeenNotificationsCount();
addNotSeenNotificationsCountListener emits the current count after initialization, then every change. Refresh it with updateNotSeenNotificationsCount():
import {
addNotSeenNotificationsCountListener,
updateNotSeenNotificationsCount,
} from '@octopus-community/react-native';
const subscription = addNotSeenNotificationsCountListener((count) => {
// Update your badge
});
await updateNotSeenNotificationsCount();
// When you no longer need it:
subscription.remove();
Subscribe to OctopusSDK.OnNotSeenNotificationsCount and refresh it with UpdateNotSeenNotificationsCount():
OctopusSDK.OnNotSeenNotificationsCount += (int count) =>
{
// Update your badge
};
OctopusSDK.UpdateNotSeenNotificationsCount();
Behavior and limits
- The SDK never requests the notification permission and never shows a prompt.
- Octopus sends data-only FCM messages to Android apps: the notification shows only if your app posts it, in the foreground and in the background alike.
isOctopusNotificationandgetOctopusNotificationaccept both payload shapes: the flat FCM data map in an Android app and the APNsuserInfoin an iOS app. Pass the payload as you receive it.
Troubleshooting
| Symptom | Cause and fix |
|---|---|
| The token registers on iOS but no notification arrives | An FCM token was registered. Register the APNs device token (getAPNSToken() with Firebase). |
| No token on the iOS Simulator | The Simulator never issues an APNs token; test token registration on a device. You can test the tap path on the Simulator with xcrun simctl push booted <bundleId> <payload.apns>. |
| No notification on Android, even in the background | Octopus messages are data-only: post the notification from your messaging service or background handler (step 2). Check also that the credentials you sent are a service account key, not google-services.json. |
| Unity, Android: the sample service never receives messages | The manifest of the sample was not merged. Move OctopusPushSample.androidlib and OctopusMessagingService.java to Assets/Plugins/Android/. |
Next steps
- Display the community: open the Octopus UI from your app.
- Navigation and links: control where the Octopus screens open.
- Theming: match the community to your brand.