跳到主要内容
about

The Notification module provides functionality for receiving and handling push notifications and silent notifications from the ESP RainMaker Cloud. Notifications enable real-time updates about device events, sharing invitations, and other system events.

Notifications are implemented across three layers:

  • Native Layer: Handles platform-specific notification services (APNS for iOS, FCM for Android)
  • SDK Layer: Provides notification adapters and event handling
  • CDF Layer: Manages platform endpoints and node update subscriptions

Notifications work only after the device token is properly registered with the ESP RainMaker backend.


Notifications

This guide explains how the Notification module works, focusing on configuration, device token setup, push notifications, silent notifications, and how they integrate with the native layer, SDK, and CDF.

User Flow Overview

The Notification module has three main flows:

  1. Device Token Registration: App initialization → Native token generation → Platform endpoint creation → Backend registration
  2. Push Notifications: Cloud event → Native notification service → React Native bridge → User notification display
  3. Silent Notifications: Cloud event → Native notification service → React Native bridge → Application data update

Architecture Overview

Components Structure

Native Layer (iOS):
ios/
├── APP/AppDelegate.mm # iOS app delegate with notification handlers
└── Notification/
└── ESPNotificationModule.swift # React Native bridge for iOS notifications

Native Layer (Android):
android/app/src/main/java/com/app/notification/
├── ESPNotificationModule.kt # React Native bridge for Android notifications
├── FCMService.kt # Firebase Cloud Messaging service
├── ESPNotificationQueue.kt # Notification queue management
└── ESPNotificationHelper.kt # Notification display helper

SDK Layer:
adaptors/implementations/
└── ESPNotificationAdapter.ts # Notification adapter for SDK integration

CDF Layer:
utils/
└── notifications.ts # Platform endpoint creation and CDF integration

1. Device Token Setup Process

The device token is essential for notifications to work. The token identifies the device to the notification service (APNS for iOS, FCM for Android) and must be registered with the ESP RainMaker backend.

Step-by-Step Token Registration Flow

iOS Token Registration

AppDelegate.mm handles iOS notification registration:

// Request notification permissions
- (void)registerForRemoteNotifications {
UNUserNotificationCenter *center = [UNUserNotificationCenter currentNotificationCenter];
center.delegate = self;

[center requestAuthorizationWithOptions:
(UNAuthorizationOptionAlert | UNAuthorizationOptionSound | UNAuthorizationOptionBadge)
completionHandler:^(BOOL granted, NSError * _Nullable error) {
if (granted) {
dispatch_async(dispatch_get_main_queue(), ^{
[[UIApplication sharedApplication] registerForRemoteNotifications];
});
}
}];
}

// Receive device token from APNS
- (void)application:(UIApplication *)application
didRegisterForRemoteNotificationsWithDeviceToken:(NSData *)deviceToken {
// Convert token to hex string
const unsigned char *tokenBytes = (const unsigned char *)[deviceToken bytes];
NSMutableString *deviceTokenString = [NSMutableString string];
for (NSUInteger i = 0; i < deviceToken.length; i++) {
[deviceTokenString appendFormat:@"%02x", tokenBytes[i]];
}

// Store token in ESPNotificationModule
[[ESPNotificationModule shared] setDeviceToken:deviceTokenString];
}

ESPNotificationModule.swift stores and retrieves the token:

// Store device token
@objc(setDeviceToken:)
func setDeviceToken(_ token: String) {
UserDefaults.standard.setValue(token, forKey: deviceTokenKey)
}

// Retrieve device token
@objc(getDeviceToken:reject:)
func getDeviceToken(resolve: @escaping RCTPromiseResolveBlock,
reject: @escaping RCTPromiseRejectBlock) {
if let token = UserDefaults.standard.value(forKey: deviceTokenKey) {
resolve(token)
} else {
reject("error", "Device token not set", nil)
}
}

Android Token Registration

ESPNotificationModule.kt retrieves FCM token:

@ReactMethod
fun getDeviceToken(promise: Promise) {
FirebaseMessaging.getInstance().token.addOnCompleteListener { task ->
if (!task.isSuccessful) {
promise.reject("FCM_TOKEN_ERROR", task.exception?.message)
return@addOnCompleteListener
}
promise.resolve(task.result)
}
}

FCMService.kt handles token refresh:

override fun onNewToken(token: String) {
super.onNewToken(token)
// Token is automatically retrieved via ESPNotificationModule.getDeviceToken()
// when the app requests it
}

Platform Endpoint Creation

After obtaining the device token, the app creates a platform endpoint with the CDF SDK:

// utils/notifications.ts
export const createPlatformEndpoint = async (store: CDF): Promise<void> => {
const { user, platform, deviceToken } = await validate(store);

const endpointConfig: PlatformEndpointConfig = {
platform, // "APNS_NOVA" for iOS, "GCM_NOVA" for Android
deviceToken, // Device-specific token from native layer
};

// Register endpoint with ESP RainMaker Cloud
await user.createPlatformEndpoint(endpointConfig);

// Subscribe to node updates - this enables silent notifications
user.subscribe(ESPRMEventType.nodeUpdates, (event: any) => {
store.subscriptionStore.nodeUpdates.listen(event);
});
};

When Token Registration Happens

The platform endpoint is created automatically during the post-login pipeline:

// utils/postLoginPipeline.ts
const postLoginSteps: PipelineStep[] = [
{
name: "refreshESPRMUser",
run: refreshESPRMUser,
},
{
name: "createPlatformEndpoint",
dependsOn: ["refreshESPRMUser"],
optional: true,
background: true,
run: () => createPlatformEndpoint(store),
},
// ... other steps
];

The endpoint is also created when the app initializes and detects a logged-in user:

// app/index.tsx
useEffect(() => {
if (store && isInitialized && store?.userStore.user) {
initNotification();
}
}, [store, isInitialized, store?.userStore.user]);

const initNotification = async () => {
try {
await createPlatformEndpoint(store);
} catch (err) {
console.error("Failed to initialize notification");
}
};

2. Push Notifications

Push notifications are visible notifications that appear to users, typically showing device events like device added, removed, connected, or disconnected.

Push Notification Flow

iOS Push Notification Handling

AppDelegate.mm handles notifications in different app states:

// Handle notification when app is in foreground
- (void)userNotificationCenter:(UNUserNotificationCenter *)center
willPresentNotification:(UNNotification *)notification
withCompletionHandler:(void (^)(UNNotificationPresentationOptions))completionHandler {
NSDictionary *userInfo = notification.request.content.userInfo;

// Send notification data to React Native
[[ESPNotificationModule shared] handlePushNotification:userInfo];

// Show notification even when app is in foreground
completionHandler(UNNotificationPresentationOptionList |
UNNotificationPresentationOptionBanner |
UNNotificationPresentationOptionSound |
UNNotificationPresentationOptionBadge);
}

// Handle notification tap/interaction
- (void)userNotificationCenter:(UNUserNotificationCenter *)center
didReceiveNotificationResponse:(UNNotificationResponse *)response
withCompletionHandler:(void (^)(void))completionHandler {
NSDictionary *userInfo = response.notification.request.content.userInfo;
[[ESPNotificationModule shared] handlePushNotification:userInfo];
completionHandler();
}

ESPNotificationModule.swift forwards notifications to React Native:

@objc(handlePushNotification:)
func handlePushNotification(_ userInfo: [String: Any]) {
if sendEvent {
sendEvent(withName: self.eventKey, body: userInfo)
}
}

Android Push Notification Handling

FCMService.kt processes incoming FCM messages:

override fun onMessageReceived(remoteMessage: RemoteMessage) {
val notificationData = remoteMessage.data.toMutableMap()

// Forward to React Native
ESPNotificationQueue.addNotification(notificationData)

// Process and display system notification
processSystemNotification(remoteMessage.data)
}

private fun processSystemNotification(data: Map<String, String>) {
val eventPayload = data[KEY_EVENT_DATA_PAYLOAD]
val title = data[KEY_TITLE] ?: DEFAULT_TITLE
val body = data[KEY_BODY] ?: DEFAULT_BODY

val eventJson = JSONObject(eventPayload)
val eventType = eventJson.optString(KEY_EVENT_TYPE)

// Map event types to notification channels
val mappedEventType = when (eventType) {
"rmaker.event.user_node_added" -> "node_added"
"rmaker.event.user_node_removed" -> "node_removed"
"rmaker.event.node_connected" -> "node_online"
"rmaker.event.node_disconnected" -> "node_offline"
else -> null
}

if (mappedEventType != null) {
ESPNotificationHelper.showEventNotification(this, mappedEventType, title, body)
}
}

React Native Notification Listener

ESPNotificationAdapter.ts sets up the notification listener:

addNotificationListener: async (
callback: (data: Record<string, any>) => void
): Promise<() => void> => {
// Listen for incoming notifications
const notificationListener = DeviceEventEmitter.addListener(
"ESPNotificationModule",
(data: Record<string, any>) => {
callback(data);
}
);

ESPNotificationAdapter.currentListener = notificationListener;

// Return cleanup function
return () => {
ESPNotificationAdapter.removeNotificationListener();
};
}

The adapter is configured in the SDK config:

// rainmaker.config.ts
export const SDKConfig = {
// ... other config
notificationAdapter: ESPNotificationAdapter,
};

Supported Push Notification Events

The following ESP RainMaker events trigger push notifications:

  • rmaker.event.user_node_added: Device added to user account
  • rmaker.event.user_node_removed: Device removed from user account
  • rmaker.event.node_connected: Device came online
  • rmaker.event.node_disconnected: Device went offline

3. Silent Notifications

Silent notifications update application data in the background without showing a visible notification to the user. They are used to keep the app state synchronized with the cloud.

Silent Notification Flow

iOS Silent Notification Handling

AppDelegate.mm handles silent notifications separately:

// Silent notification handling (background fetch)
- (void)application:(UIApplication *)application
didReceiveRemoteNotification:(NSDictionary *)userInfo
fetchCompletionHandler:(void (^)(UIBackgroundFetchResult))completionHandler {
[[ESPNotificationModule shared] handleSilentNotification:userInfo];
completionHandler(UIBackgroundFetchResultNewData);
}

ESPNotificationModule.swift processes silent notifications:

@objc(handleSilentNotification:)
func handleSilentNotification(_ userInfo: [String: Any]) {
// Forward to React Native without showing UI
if sendEvent {
sendEvent(withName: self.eventKey, body: userInfo)
}
}

Android Silent Notification Handling

FCMService.kt handles both push and silent notifications:

override fun onMessageReceived(remoteMessage: RemoteMessage) {
val notificationData = remoteMessage.data.toMutableMap()

// All notifications are forwarded to React Native
ESPNotificationQueue.addNotification(notificationData)

// Only show system notification if it's a push notification
// Silent notifications don't have notification payload
if (remoteMessage.notification != null) {
processSystemNotification(remoteMessage.data)
}
}

How Silent Notifications Update Application Data

When a silent notification is received, it contains node update information. The CDF SDK processes these updates:

// utils/notifications.ts
export const createPlatformEndpoint = async (store: CDF): Promise<void> => {
// ... endpoint creation ...

// Subscribe to node updates - this is the key for silent notifications
user.subscribe(ESPRMEventType.nodeUpdates, (event: any) => {
store.subscriptionStore.nodeUpdates.listen(event);
});
};

The nodeUpdates subscription handler processes the event:

  1. Event Reception: Silent notification arrives with node update data
  2. CDF Processing: store.subscriptionStore.nodeUpdates.listen(event) processes the event
  3. Store Update: Node store is updated with new device states, parameters, or metadata
  4. UI Update: React components subscribed to the store automatically re-render with updated data

Example: Device State Update via Silent Notification

When a device's state changes (e.g., a light is turned on):

  1. ESP32 Device sends state update to ESP RainMaker Cloud via MQTT
  2. Cloud sends silent notification to all registered devices
  3. Native Layer receives notification and forwards to React Native
  4. CDF Store processes the nodeUpdates event
  5. Node Store updates the specific device's state
  6. React Components using useCDF() hook automatically receive updated data
  7. UI reflects the new device state without user interaction

Notification Queue Management

ESPNotificationQueue (Android) manages notifications when the React Native bridge isn't ready:

// ESPNotificationModule.kt
@ReactMethod
fun toggleNotificationListener(enable: Boolean) {
synchronized(this) {
isNotificationListenerActive = enable
}

if (enable) {
// Flush any queued notifications
ESPNotificationQueue.flushPendingNotifications()
}
}

This ensures notifications aren't lost during app initialization.

4. Notification Configuration

SDK Configuration

Notifications are configured in the SDK config:

// rainmaker.config.ts
import { ESPNotificationAdapter } from "@/adaptors/implementations/ESPNotificationAdapter";

export const SDKConfig = {
// ... other config
notificationAdapter: ESPNotificationAdapter,
};

Platform Identifiers

Each platform has a specific identifier used by the backend:

  • iOS: "APNS_NOVA" - Apple Push Notification Service
  • Android: "GCM_NOVA" - Google Cloud Messaging (Firebase Cloud Messaging)

These identifiers are retrieved from the native modules:

// iOS: ESPNotificationModule.swift
func getNotificationPlatform(resolve: @escaping RCTPromiseResolveBlock,
reject: @escaping RCTPromiseRejectBlock) {
resolve("APNS_NOVA")
}

// Android: ESPNotificationModule.kt
fun getNotificationPlatform(promise: Promise) {
promise.resolve("GCM_NOVA")
}

Notification Permissions

iOS requires explicit permission request:

[center requestAuthorizationWithOptions:
(UNAuthorizationOptionAlert | UNAuthorizationOptionSound | UNAuthorizationOptionBadge)
completionHandler:^(BOOL granted, NSError * _Nullable error) {
// Handle permission result
}];

Android requires POST_NOTIFICATIONS permission (Android 13+):

<!-- AndroidManifest.xml -->
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />

5. Notification Event Types

Event Payload Structure

Notifications contain an event_data_payload with the following structure:

{
"event_data_payload": "{\"event_type\":\"rmaker.event.node_connected\",\"node_id\":\"...\",\"timestamp\":...}",
"title": "Device Connected",
"body": "Your device is now online"
}

Supported Event Types

Event TypeDescriptionNotification Type
rmaker.event.user_node_addedDevice added to accountPush
rmaker.event.user_node_removedDevice removed from accountPush
rmaker.event.node_connectedDevice came onlinePush
rmaker.event.node_disconnectedDevice went offlinePush
rmaker.event.node_params_changedDevice parameters changedSilent
rmaker.event.node_metadata_changedDevice metadata changedSilent
rmaker.event.sharing_requestSharing invitation receivedPush

6. Troubleshooting

Notifications Not Working

  1. Check Device Token: Ensure the device token is properly retrieved and stored

    const token = await fetchDeviceToken();
    console.log("Device token:", token);
  2. Verify Platform Endpoint: Check if the endpoint was created successfully

    try {
    await createPlatformEndpoint(store);
    console.log("Platform endpoint created");
    } catch (error) {
    console.error("Failed to create endpoint:", error);
    }
  3. Check Permissions: Ensure notification permissions are granted

    • iOS: Check in Settings → [App Name] → Notifications
    • Android: Check in Settings → Apps → [App Name] → Notifications
  4. Verify Notification Listener: Ensure the listener is active

    ESPNotificationAdapter.addNotificationListener((data) => {
    console.log("Notification received:", data);
    });

Token Refresh

Device tokens can change in the following scenarios:

  • App reinstalled
  • App data cleared
  • App updated
  • Firebase SDK updated (Android)
  • Security refresh

The app should handle token refresh by recreating the platform endpoint when a new token is detected.

Silent Notifications Not Updating Data

  1. Verify Subscription: Ensure nodeUpdates subscription is active

    user.subscribe(ESPRMEventType.nodeUpdates, (event: any) => {
    store.subscriptionStore.nodeUpdates.listen(event);
    });
  2. Check Event Processing: Verify events are being processed

    user.subscribe(ESPRMEventType.nodeUpdates, (event: any) => {
    console.log("Node update event:", event);
    store.subscriptionStore.nodeUpdates.listen(event);
    });
  3. Verify Store Updates: Check if the store is actually updating

    const { nodeStore } = useCDF();
    console.log("Node list:", nodeStore.nodeList);

Summary

The Notification module provides a complete solution for real-time updates in the ESP RainMaker app:

  1. Device Token Setup: Native modules retrieve platform-specific tokens (APNS/FCM) and register them with the backend
  2. Push Notifications: Visible notifications inform users about device events
  3. Silent Notifications: Background updates keep application data synchronized
  4. CDF Integration: Node updates are automatically processed and reflected in the UI
  5. Cross-Platform: Unified API across iOS and Android with platform-specific implementations

Notifications are essential for maintaining real-time synchronization between the ESP RainMaker Cloud and the mobile app, ensuring users always have the latest device information.