Push notifications
Expo push tokens, permissions, listeners, and click tracking with @motisig/expo-motisig-sdk.
@motisig/expo-motisig-sdk is the official MotiSig AI JavaScript-only client that uses expo-notifications for push tokens and delivery callbacks; no separate native MotiSig AI library is required.
Channels
- iOS: APNs via Expo's relay (
pushType: 'expo',devicePlatform: 'ios'). - Android: FCM via Expo's relay (
pushType: 'expo',devicePlatform: 'android').
The MotiSig AI backend speaks Expo natively for this SDK.
Permissions
initialize calls requestNotificationPermissions() unless skipPermissionRequest: true. You can also call it manually:
import { requestNotificationPermissions, getNotificationPermissionStatus } from '@motisig/expo-motisig-sdk';
const granted = await requestNotificationPermissions();
const status = await getNotificationPermissionStatus();Expo push token
const token = await motisig.getExpoPushToken();Returns the Expo push token string or null. Returns null on simulators (expo-device reports isDevice === false) and when the EAS project id cannot be resolved (see Configuration).
The SDK calls getExpoPushToken() for you inside setUser and on token refresh.
Push subscription lifecycle
When a token is known and a user is set, the SDK upserts the subscription via POST /users/{id}/push-subscriptions:
{
devicePlatform: 'ios' | 'android',
pushType: 'expo',
token: '<expo push token>',
permission: 'granted' | 'declined' | 'unknown',
enabled: <customer flag>,
}It removes the previous token's subscription before upserting a new one when the token changes.
setNotificationEnabled(enabled)
await motisig.setNotificationEnabled(false);Customer-controlled flag, persisted via AsyncStorage when installed. Sends PATCH …/push-subscriptions with the new enabled value. Does not change the OS notification permission. motisig.isNotificationEnabled returns the current value.
Permission patch on resume
The SDK subscribes to AppState. On every transition to active, it reads the OS permission (getPermissionsAsync). If the value differs from the last synced one, it sends PATCH …/push-subscriptions with the new permission. The customer enabled flag is not inferred from permission.
Listener API
const unsubscribe = motisig.addListener((event) => {
switch (event.type) {
case 'foreground_notification':
// event.notification, event.data
break;
case 'notification_response':
// event.response, event.data, event.wasForeground
break;
case 'token_refresh':
// event.token, event.previousToken
break;
}
});
// later:
unsubscribe();Behavior:
- All listeners receive every event in registration order (no
orderparameter — use thetypefield to filter). - Foreground notifications carrying
data.suppressForeground === trueskip theforeground_notificationevent but still run click-tracking when applicable. - The SDK keeps a bounded ring buffer of the last 10 received foreground notification ids so it can decide
wasForegroundon a subsequent tap.
Cold-start tap handling
initialize reads Notifications.getLastNotificationResponseAsync() once. If a tap was waiting from a cold start, the SDK runs click tracking using the same code path as live notification_response events. You don't need to do anything extra for this to work.
The last setUser id is persisted (AsyncStorage when installed) so a cold start after relaunch can attach clicks to the correct user even before your UI calls setUser again — initialize reloads the persisted id before handling the pending response.
Click tracking
POST /track/click is enqueued through an internal ClickDispatcher (persistent when AsyncStorage is available) when:
- the payload contains
messageId(ormessage_id), and - a user is set or becomes set before the queued click drains.
Automatic tracking runs on:
notification_response(tap) —isForegroundreflects whether the same notification was seen in foreground first (wasForeground).foreground_notification— foreground delivery withisForeground: true(unlessdata.suppressForeground === true, which skips the listener event but still enqueues click tracking when applicable).
Reliability
- Persistent FIFO — Pending clicks survive app restarts when
@react-native-async-storage/async-storageis installed. - Deduplication — The same
messageIdis not sent twice. - Retries — Transient failures use exponential backoff (defaults: 50 attempts, 1000 ms base, 60000 ms max). Configure via
clickRetryon Configuration. setUser— Flushes clicks that were queued before a user id was known (onUserSetupdates pending entries).logout()/reset()— Clear the queue and dedupe store (andlogout()clears the persisted user id).
For manual control, call motisig.trackClick(messageId, isForeground?).
Caveats
fetchDeliveredNotifications— not available. Use thenotification_responseevent for taps;expo-notificationsdoes not surface the platform "delivered list".- Use
addListenerand remember to call the returnedunsubscribefunction (no weak-reference semantics in JS).
Rich images
Banner images on iOS require a Notification Service Extension. @motisig/expo-motisig-sdk ships a default Objective-C NSE and a config plugin that wires it through expo-notification-service-extension-plugin. Full recipe: Rich notification images.
On Android, FCM auto-renders the image when the push is sent as a notification message with notification.image. No extra setup required for the banner.