Notifications
Documentation for Push Notifications, In-App Inbox, and Notification Management in Expo.
The boilerplate provides a sophisticated notification system using Expo Notifications for push and local alerts, integrated with Firebase Cloud Messaging (FCM) for remote delivery, and a dedicated Notification History for an in-app inbox experience.
Overview
Notifications in the Expo project are structured into several layers:
- Push Notifications (FCM): Remote delivery using Firebase Cloud Messaging via Expo's push service.
- Foreground Notifications: UI alerts shown when the app is active.
- Notification History: Backend-stored notification logs accessible via API.
- Preferences Management: User settings for specific notification types, persisted locally and synced to the server.
🏗 Architecture
Initialization
The notification system is initialized in app/_layout.tsx using the usePushNotifications hook. This ensures listeners for incoming notifications and user interactions are active as soon as the app starts.
// app/_layout.tsx
export default function RootLayoutContent() {
// ...
// Initialize push notifications (listeners)
usePushNotifications();
// ...
}1. Push Notifications (FCM)
Uses the expo-notifications library. The logic is encapsulated in the usePushNotifications hook located at hooks/use-push-notifications.ts.
- Token Management: The hook handles requesting permissions and retrieving the FCM token. It automatically saves the token to the
preferences-storeand syncs it to the backend when the user is authenticated. - Handling States:
- Foreground:
Notifications.setNotificationHandlerdefines how notifications appear when the app is open (alerts, sounds, badges). - Taps/Interactions:
addNotificationResponseReceivedListenercatches when a user taps a notification, allowing for deep link navigation.
- Foreground:
- Permission Flow: Managed within the hook. Permissions are typically requested during onboarding or when the user toggles notifications in settings.
2. Notification History (In-App Inbox)
Unlike ephemeral push notifications, the notification history is a persistent log of alerts stored on the backend.
- Fetching: Handled by the
useNotificationsquery inqueries/use-notifications.ts. It uses TanStack Query for efficient caching and state management. - Actions: Marking notifications as read or deleting them is handled by mutations in
mutations/use-notification-mutations.ts. - UI: The
app/notifications.tsxscreen provides a rich interface for browsing, reading, and managing these notifications.
3. Settings & Preferences
Managed via the usePreferencesStore (Zustand).
- Persistence: Settings like "Push Enabled", "Promotions", and "Vibration" are saved to device storage using
expo-secure-storeorAsyncStorage. - Server Sync: When a user changes a preference, the store automatically debounces and syncs the new state to the backend using
api.put(ENDPOINTS.NOTIFICATION_PREFERENCES). - Offline Support: If sync fails (e.g., no internet), the change is marked as pending and retried once the connection is restored.
🛠 Configuration
Expo & Firebase Setup
- app.json: Configure the
expo-notificationsplugin:{ "expo": { "plugins": [ [ "expo-notifications", { "icon": "./assets/images/notification-icon.png", "color": "#ffffff", "defaultChannel": "default" } ] ] } } - Credentials:
- Place
google-services.jsonin the root (for Android). - Place
GoogleService-Info.plistin the root (for iOS).
- Place
- EAS Build: Ensure push notifications are enabled in your Expo dashboard and proper credentials (FCM Server Key) are uploaded.
🚀 Usage
Using the Push Notification Hook
To manually request permissions or get the push token:
import { usePushNotifications } from '@/hooks/use-push-notifications';
const MyComponent = () => {
const { registerForPushNotificationsAsync, expoPushToken } = usePushNotifications();
const handleEnable = async () => {
const token = await registerForPushNotificationsAsync();
console.log('Registered with token:', token);
};
};Accessing Notification Settings
Use the preferences store to read or update settings:
import { usePreferencesStore } from '@/store/preferences-store';
const { pushEnabled, setPushEnabled, setPromotions } = usePreferencesStore();
// Update a setting
await setPromotions(true); // This automatically syncs to backendFetching & Managing History
Interact with the in-app inbox using TanStack Query hooks:
import { useNotifications } from '@/queries/use-notifications';
import { useMarkNotificationReadMutation } from '@/mutations/use-notification-mutations';
const NotificationsList = () => {
const { data: notifications } = useNotifications();
const markRead = useMarkNotificationReadMutation();
const handleSelect = (id: string) => {
markRead.mutate(id);
};
// Render your list...
};🔗 Deep Linking
When a notification is tapped, the responseListener in use-push-notifications.ts is triggered. You can extend this logic to navigate to specific routes based on the data payload:
// Inside use-push-notifications.ts
Notifications.addNotificationResponseReceivedListener(response => {
const data = response.notification.request.content.data;
if (data.url) {
router.push(data.url);
}
});