The Incidents App uses Expo Notifications to deliver real-time push notifications for incident updates and status changes.
Overview
Push notifications keep both guests and staff members informed about important incident updates without requiring them to actively check the app.
Notifications are permission-based and users can control their notification preferences through the app settings.
Notification Types
Guest Notifications
Incident received confirmation
Status updates (accepted, in progress)
Resolution notifications
Staff Notifications
New incidents in area
Task assignments
High-priority alerts
Implementation
The notification system is implemented using the useNotifications hook:
import * as Notifications from 'expo-notifications'
import * as SecureStore from 'expo-secure-store'
import { useState } from 'react'
import { Alert , Linking , Platform } from 'react-native'
import * as Haptics from 'expo-haptics'
export const useNotifications = ( storageKey : string ) => {
const [ notifications , setNotifications ] = useState ( false )
const initializeNotifications = async () => {
const storedValue = await SecureStore . getItemAsync ( storageKey )
if ( storedValue === "true" ) {
setNotifications ( true )
}
const { status } = await Notifications . getPermissionsAsync ()
if ( status === "granted" ) {
setNotifications ( true )
await SecureStore . setItemAsync ( storageKey , "true" )
} else {
setNotifications ( false )
await SecureStore . setItemAsync ( storageKey , "false" )
}
}
return {
notifications ,
setNotifications ,
initializeNotifications ,
handleNotificationToggle ,
}
}
Permission Management
Requesting Permissions
The hook handles permission requests with haptic feedback:
const handleNotificationToggle = async ( value : boolean ) => {
if ( value ) {
const { status : existingStatus } =
await Notifications . getPermissionsAsync ()
if ( existingStatus !== "granted" ) {
Haptics . impactAsync ( Haptics . ImpactFeedbackStyle . Light )
const { status } = await Notifications . requestPermissionsAsync ()
if ( status !== "granted" ) {
setNotifications ( false )
await SecureStore . setItemAsync ( storageKey , "false" )
Haptics . notificationAsync ( Haptics . NotificationFeedbackType . Error )
return
}
}
setNotifications ( true )
await SecureStore . setItemAsync ( storageKey , "true" )
Haptics . notificationAsync ( Haptics . NotificationFeedbackType . Success )
}
}
Haptic feedback provides tactile confirmation when toggling notification settings.
Permission Flow
Check Existing Permission
Query the current notification permission status using getPermissionsAsync()
Request If Needed
If not granted, request permission with requestPermissionsAsync()
Store Preference
Save the user’s preference in SecureStore for persistence
Provide Feedback
Use haptic feedback to confirm the action
Disabling Notifications
When users want to disable notifications, they’re directed to system settings:
if ( ! value ) {
Haptics . impactAsync ( Haptics . ImpactFeedbackStyle . Medium )
Alert . alert (
"Desactivar Notificaciones" ,
"Para desactivar completamente las notificaciones, ve a Configuración > Aplicaciones > Amanera > Notificaciones y desactívalas." ,
[
{
text: "Entendido" ,
style: "cancel" ,
},
{
text: "Ir a Configuración" ,
onPress : () => {
if ( Platform . OS === "ios" ) {
Linking . openURL ( "app-settings:" )
} else {
Linking . openSettings ()
}
},
},
],
)
}
On both iOS and Android, disabling notifications at the system level is more reliable than in-app toggles. This ensures users have full control and prevents the app from sending notifications even if the preference gets reset.
iOS
if ( Platform . OS === "ios" ) {
Linking . openURL ( "app-settings:" )
}
Uses app-settings: URL scheme
Opens app-specific settings directly
Follows iOS design guidelines
Android
if ( Platform . OS === "android" ) {
Linking . openSettings ()
}
Opens system settings
Users navigate to app notifications
Follows Material Design patterns
Persistence
Notification preferences are stored using SecureStore:
// Save preference
await SecureStore . setItemAsync ( storageKey , "true" )
// Load preference on initialization
const storedValue = await SecureStore . getItemAsync ( storageKey )
if ( storedValue === "true" ) {
setNotifications ( true )
}
Each user role (guest/employee) has a separate storage key for notification preferences.
Usage Example
Integrating the hook in a settings screen:
import { useNotifications } from '@/hooks/settings/use-notifications'
export default function SettingsScreen () {
const {
notifications ,
initializeNotifications ,
handleNotificationToggle
} = useNotifications ( 'guest_notifications' )
useEffect (() => {
initializeNotifications ()
}, [])
return (
< Switch
value = { notifications }
onValueChange = { handleNotificationToggle }
/>
)
}
Storage Keys
Guest Notifications guest_notifications
Employee Notifications employee_notifications
Notification Configuration
Global notification configuration in app.json:
{
"expo" : {
"notification" : {
"icon" : "./assets/notification-icon.png" ,
"color" : "#0099ff" ,
"androidMode" : "default" ,
"androidCollapsedTitle" : "#{unread_notifications} new notifications"
}
}
}
Best Practices
1. Initialize on Mount
useEffect (() => {
initializeNotifications ()
}, [])
Always initialize notifications when the settings screen mounts to sync with system permissions.
2. Use Haptic Feedback
Haptics . notificationAsync ( Haptics . NotificationFeedbackType . Success )
Provide tactile feedback to confirm user actions.
3. Handle Permission Denial
if ( status !== "granted" ) {
setNotifications ( false )
Haptics . notificationAsync ( Haptics . NotificationFeedbackType . Error )
// Inform user why notifications are important
}
Gracefully handle when users deny permissions.
4. Persist Preferences
Always save preferences to SecureStore for persistence across app restarts.
Permission States
User has allowed notifications. App can send and receive push notifications.
User has explicitly denied notifications. App should respect this choice and not request again.
User hasn’t been asked yet. Good time to request permission with context.
Notification Payload Structure
Example notification payload for incident updates:
interface NotificationPayload {
title : string
body : string
data : {
incidentId : string
status : string
priority : 'baja' | 'media' | 'alta'
type : 'status_update' | 'new_incident' | 'resolution'
}
}
Handling Received Notifications
Listening for notifications while app is in foreground:
Notifications . setNotificationHandler ({
handleNotification : async () => ({
shouldShowAlert: true ,
shouldPlaySound: true ,
shouldSetBadge: true ,
}),
})
Notifications . addNotificationReceivedListener (( notification ) => {
const { incidentId , type } = notification . request . content . data
// Handle notification based on type
})
Deep Linking
Notifications can deep link to specific incidents:
Notifications . addNotificationResponseReceivedListener (( response ) => {
const { incidentId } = response . notification . request . content . data
if ( incidentId ) {
router . push ( `/incidents/ ${ incidentId } ` )
}
})
Testing Notifications
Send a test notification during development:
await Notifications . scheduleNotificationAsync ({
content: {
title: "Test Notification" ,
body: "This is a test notification" ,
data: { incidentId: "123" },
},
trigger: null , // Send immediately
})
Test notifications work in development mode on physical devices. Simulators have limited notification support.
Incident Tracking See how notifications integrate with incidents
Real-Time Updates Learn about real-time data synchronization