On this page
🔔 Notifications System
Index
Notification Options
Local Notifications
Push Notifications with Expo
Push Notifications with Firebase
Notifications Context
Permission Handling
Deep Linking from Notifications
Notification Options
Option Type Best for Pros Cons Local Notifications Local Reminders, alarms No server, offline Not remote Expo Push Push Expo apps, prototyping Easy setup, free Expo only Firebase FCM Push Production apps Scalable, analytics More setup OneSignal Push Marketing, segmentation Powerful dashboard Cost at scale
Local Notifications
Installation
npx expo install expo-notifications
Basic Configuration
import * as Notifications from 'expo-notifications' ;
import { Platform } from 'react-native' ;
Notifications. setNotificationHandler ( {
handleNotification : async ( ) => ( {
shouldShowAlert: true ,
shouldPlaySound: true ,
shouldSetBadge: true ,
} ) ,
} ) ;
export async function requestPermissions ( ) {
const { status: existingStatus } = await Notifications. getPermissionsAsync ( ) ;
let finalStatus = existingStatus;
if ( existingStatus !== 'granted' ) {
const { status } = await Notifications. requestPermissionsAsync ( ) ;
finalStatus = status;
}
return finalStatus === 'granted' ;
}
Programar Notificación Local
import * as Notifications from 'expo-notifications' ;
export async function scheduleLocalNotification ( {
title,
body,
data,
trigger,
} : {
title: string ;
body: string ;
data? : Record< string , unknown > ;
trigger? : Notifications. NotificationTriggerInput;
} ) {
const id = await Notifications. scheduleNotificationAsync ( {
content: {
title,
body,
data,
sound: true ,
} ,
trigger: trigger ?? null ,
} ) ;
return id;
}
export async function scheduleDelayedNotification (
title: string ,
body: string ,
seconds: number
) {
return scheduleLocalNotification ( {
title,
body,
trigger: { seconds, repeats: false } ,
} ) ;
}
export async function scheduleDailyNotification (
title: string ,
body: string ,
hour: number ,
minute: number
) {
return scheduleLocalNotification ( {
title,
body,
trigger: {
hour,
minute,
repeats: true ,
} ,
} ) ;
}
export async function cancelNotification ( id: string ) {
await Notifications. cancelScheduledNotificationAsync ( id) ;
}
export async function cancelAllNotifications ( ) {
await Notifications. cancelAllScheduledNotificationsAsync ( ) ;
}
Hook para Notificaciones Locales
import { useEffect, useRef, useState } from 'react' ;
import * as Notifications from 'expo-notifications' ;
import { useMutation } from '@tanstack/react-query' ;
import {
requestPermissions,
scheduleLocalNotification,
cancelNotification,
cancelAllNotifications
} from '@/lib/notifications' ;
export function useLocalNotifications ( ) {
const [ hasPermission, setHasPermission] = useState ( false ) ;
const notificationListener = useRef < Notifications. Subscription> ( ) ;
const responseListener = useRef < Notifications. Subscription> ( ) ;
useEffect ( ( ) => {
requestPermissions ( ) . then ( setHasPermission) ;
notificationListener. current = Notifications. addNotificationReceivedListener (
( notification) => {
console . log ( 'Notification received:' , notification) ;
}
) ;
responseListener. current = Notifications. addNotificationResponseReceivedListener (
( response) => {
console . log ( 'Notification tapped:' , response) ;
const data = response. notification. request. content. data;
}
) ;
return ( ) => {
if ( notificationListener. current) {
Notifications. removeNotificationSubscription ( notificationListener. current) ;
}
if ( responseListener. current) {
Notifications. removeNotificationSubscription ( responseListener. current) ;
}
} ;
} , [ ] ) ;
const scheduleMutation = useMutation ( {
mutationFn : ( params: { title: string ; body: string ; seconds? : number } ) =>
scheduleLocalNotification ( {
title: params. title,
body: params. body,
trigger: params. seconds ? { seconds: params. seconds } : null ,
} ) ,
} ) ;
const cancelMutation = useMutation ( {
mutationFn: cancelNotification,
} ) ;
return {
hasPermission,
schedule: scheduleMutation. mutate,
cancel: cancelMutation. mutate,
cancelAll: cancelAllNotifications,
isScheduling: scheduleMutation. isPending,
} ;
}
Push Notifications con Expo
Obtener Push Token
import * as Notifications from 'expo-notifications' ;
import * as Device from 'expo-device' ;
import { Platform } from 'react-native' ;
import Constants from 'expo-constants' ;
export async function registerForPushNotifications ( ) : Promise < string | null > {
if ( ! Device. isDevice) {
console . log ( 'Push notifications require a physical device' ) ;
return null ;
}
const { status: existingStatus } = await Notifications. getPermissionsAsync ( ) ;
let finalStatus = existingStatus;
if ( existingStatus !== 'granted' ) {
const { status } = await Notifications. requestPermissionsAsync ( ) ;
finalStatus = status;
}
if ( finalStatus !== 'granted' ) {
console . log ( 'Permission not granted for push notifications' ) ;
return null ;
}
if ( Platform. OS === 'android' ) {
await Notifications. setNotificationChannelAsync ( 'default' , {
name: 'default' ,
importance: Notifications. AndroidImportance. MAX ,
vibrationPattern: [ 0 , 250 , 250 , 250 ] ,
lightColor: '#FF6B35' ,
} ) ;
}
const projectId = Constants. expoConfig?. extra?. eas?. projectId;
const token = await Notifications. getExpoPushTokenAsync ( { projectId } ) ;
return token. data;
}
Enviar Push desde Backend
interface PushMessage {
to: string ;
title: string ;
body: string ;
data? : Record< string , unknown > ;
sound? : 'default' | null ;
badge? : number ;
}
export async function sendPushNotification ( message: PushMessage) {
const response = await fetch ( 'https://exp.host/--/api/v2/push/send' , {
method: 'POST' ,
headers: {
'Content-Type' : 'application/json' ,
} ,
body: JSON . stringify ( {
to: message. to,
title: message. title,
body: message. body,
data: message. data,
sound: message. sound ?? 'default' ,
badge: message. badge,
} ) ,
} ) ;
return response. json ( ) ;
}
export async function sendPushToMultiple ( tokens: string [ ] , message: Omit< PushMessage, 'to' > ) {
const messages = tokens. map ( token => ( {
... message,
to: token,
} ) ) ;
const response = await fetch ( 'https://exp.host/--/api/v2/push/send' , {
method: 'POST' ,
headers: {
'Content-Type' : 'application/json' ,
} ,
body: JSON . stringify ( messages) ,
} ) ;
return response. json ( ) ;
}
Push Notifications con Firebase
Instalación
npx expo install @react-native-firebase/app @react-native-firebase/messaging
Configuración
import messaging from '@react-native-firebase/messaging' ;
import { Platform } from 'react-native' ;
export async function requestFirebasePermission ( ) {
const authStatus = await messaging ( ) . requestPermission ( ) ;
const enabled =
authStatus === messaging. AuthorizationStatus. AUTHORIZED ||
authStatus === messaging. AuthorizationStatus. PROVISIONAL ;
return enabled;
}
export async function getFirebaseToken ( ) : Promise < string | null > {
const hasPermission = await requestFirebasePermission ( ) ;
if ( ! hasPermission) {
return null ;
}
const token = await messaging ( ) . getToken ( ) ;
return token;
}
export function onTokenRefresh ( callback : ( token: string ) => void ) {
return messaging ( ) . onTokenRefresh ( callback) ;
}
export function onForegroundMessage ( callback : ( message: any ) => void ) {
return messaging ( ) . onMessage ( callback) ;
}
export function setBackgroundMessageHandler ( handler : ( message: any ) => Promise < void > ) {
messaging ( ) . setBackgroundMessageHandler ( handler) ;
}
Hook para Firebase Push
import { useEffect, useState } from 'react' ;
import { useMutation } from '@tanstack/react-query' ;
import {
getFirebaseToken,
onTokenRefresh,
onForegroundMessage,
} from '@/lib/firebasePush' ;
export function useFirebasePush ( onMessage? : ( message: any ) => void ) {
const [ token, setToken] = useState < string | null > ( null ) ;
const [ hasPermission, setHasPermission] = useState ( false ) ;
useEffect ( ( ) => {
getFirebaseToken ( ) . then ( ( t) => {
if ( t) {
setToken ( t) ;
setHasPermission ( true ) ;
}
} ) ;
const unsubscribeRefresh = onTokenRefresh ( ( newToken) => {
setToken ( newToken) ;
} ) ;
const unsubscribeMessage = onForegroundMessage ( ( message) => {
console . log ( 'FCM Message:' , message) ;
onMessage?. ( message) ;
} ) ;
return ( ) => {
unsubscribeRefresh ( ) ;
unsubscribeMessage ( ) ;
} ;
} , [ onMessage] ) ;
const registerMutation = useMutation ( {
mutationFn : async ( ) => {
const newToken = await getFirebaseToken ( ) ;
if ( newToken) {
setToken ( newToken) ;
setHasPermission ( true ) ;
}
return newToken;
} ,
} ) ;
return {
token,
hasPermission,
register: registerMutation. mutate,
isRegistering: registerMutation. isPending,
} ;
}
Contexto de Notificaciones
import createContextHook from '@nkzw/create-context-hook' ;
import { useEffect, useRef, useState, useCallback } from 'react' ;
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query' ;
import * as Notifications from 'expo-notifications' ;
import AsyncStorage from '@react-native-async-storage/async-storage' ;
import { Platform } from 'react-native' ;
import { useRouter } from 'expo-router' ;
Notifications. setNotificationHandler ( {
handleNotification : async ( ) => ( {
shouldShowAlert: true ,
shouldPlaySound: true ,
shouldSetBadge: true ,
} ) ,
} ) ;
interface NotificationItem {
id: string ;
title: string ;
body: string ;
data? : Record< string , unknown > ;
read: boolean ;
createdAt: string ;
}
const STORAGE_KEY = 'app_notifications' ;
const PUSH_TOKEN_KEY = 'push_token' ;
export const [ NotificationsProvider, useNotifications] = createContextHook ( ( ) => {
const queryClient = useQueryClient ( ) ;
const router = useRouter ( ) ;
const [ pushToken, setPushToken] = useState < string | null > ( null ) ;
const [ hasPermission, setHasPermission] = useState ( false ) ;
const notificationListener = useRef < Notifications. Subscription> ( ) ;
const responseListener = useRef < Notifications. Subscription> ( ) ;
const notificationsQuery = useQuery ( {
queryKey: [ 'notifications' ] ,
queryFn: async ( ) : Promise < NotificationItem[ ] > => {
const stored = await AsyncStorage. getItem ( STORAGE_KEY ) ;
return stored ? JSON . parse ( stored) : [ ] ;
} ,
} ) ;
const saveMutation = useMutation ( {
mutationFn : async ( notifications: NotificationItem[ ] ) => {
await AsyncStorage. setItem ( STORAGE_KEY , JSON . stringify ( notifications) ) ;
return notifications;
} ,
onSuccess : ( data) => {
queryClient. setQueryData ( [ 'notifications' ] , data) ;
} ,
} ) ;
const registerPushMutation = useMutation ( {
mutationFn : async ( ) => {
const { status: existingStatus } = await Notifications. getPermissionsAsync ( ) ;
let finalStatus = existingStatus;
if ( existingStatus !== 'granted' ) {
const { status } = await Notifications. requestPermissionsAsync ( ) ;
finalStatus = status;
}
if ( finalStatus !== 'granted' ) {
return null ;
}
setHasPermission ( true ) ;
if ( Platform. OS === 'android' ) {
await Notifications. setNotificationChannelAsync ( 'default' , {
name: 'default' ,
importance: Notifications. AndroidImportance. MAX ,
} ) ;
}
const tokenData = await Notifications. getExpoPushTokenAsync ( ) ;
const token = tokenData. data;
await AsyncStorage. setItem ( PUSH_TOKEN_KEY , token) ;
setPushToken ( token) ;
return token;
} ,
} ) ;
useEffect ( ( ) => {
AsyncStorage. getItem ( PUSH_TOKEN_KEY ) . then ( ( token) => {
if ( token) {
setPushToken ( token) ;
setHasPermission ( true ) ;
}
} ) ;
notificationListener. current = Notifications. addNotificationReceivedListener (
( notification) => {
const { title, body, data } = notification. request. content;
addNotification ( {
title: title || 'Notificación' ,
body: body || '' ,
data: data as Record< string , unknown > ,
} ) ;
}
) ;
responseListener. current = Notifications. addNotificationResponseReceivedListener (
( response) => {
const data = response. notification. request. content. data;
if ( data?. route) {
router. push ( data. route as string ) ;
}
}
) ;
return ( ) => {
if ( notificationListener. current) {
Notifications. removeNotificationSubscription ( notificationListener. current) ;
}
if ( responseListener. current) {
Notifications. removeNotificationSubscription ( responseListener. current) ;
}
} ;
} , [ ] ) ;
const notifications = notificationsQuery. data || [ ] ;
const unreadCount = notifications. filter ( ( n) => ! n. read) . length;
const addNotification = useCallback (
( notification: Omit< NotificationItem, 'id' | 'read' | 'createdAt' > ) => {
const newNotification: NotificationItem = {
id: Date. now ( ) . toString ( ) ,
... notification,
read: false ,
createdAt: new Date ( ) . toISOString ( ) ,
} ;
const updated = [ newNotification, ... notifications] ;
saveMutation. mutate ( updated) ;
} ,
[ notifications, saveMutation]
) ;
const markAsRead = useCallback (
( id: string ) => {
const updated = notifications. map ( ( n) =>
n. id === id ? { ... n, read: true } : n
) ;
saveMutation. mutate ( updated) ;
} ,
[ notifications, saveMutation]
) ;
const markAllAsRead = useCallback ( ( ) => {
const updated = notifications. map ( ( n) => ( { ... n, read: true } ) ) ;
saveMutation. mutate ( updated) ;
} , [ notifications, saveMutation] ) ;
const clearAll = useCallback ( ( ) => {
saveMutation. mutate ( [ ] ) ;
} , [ saveMutation] ) ;
const scheduleLocal = useCallback (
async ( title: string , body: string , trigger? : Notifications. NotificationTriggerInput) => {
return Notifications. scheduleNotificationAsync ( {
content: { title, body, sound: true } ,
trigger: trigger ?? null ,
} ) ;
} ,
[ ]
) ;
return {
notifications,
unreadCount,
hasPermission,
pushToken,
isLoading: notificationsQuery. isLoading,
registerPush: registerPushMutation. mutateAsync,
isRegistering: registerPushMutation. isPending,
addNotification,
markAsRead,
markAllAsRead,
clearAll,
scheduleLocal,
} ;
} ) ;
Manejo de Permisos
Componente de Solicitud de Permisos
import { View, Text, StyleSheet } from 'react-native' ;
import { Bell } from 'lucide-react-native' ;
import { Button } from '@/components/Button' ;
import { useTheme } from '@/contexts/ThemeContext' ;
import { useNotifications } from '@/contexts/NotificationsContext' ;
interface Props {
onDismiss? : ( ) => void ;
}
export function NotificationPermissionPrompt ( { onDismiss } : Props) {
const { colors } = useTheme ( ) ;
const { registerPush, isRegistering, hasPermission } = useNotifications ( ) ;
if ( hasPermission) return null ;
const handleEnable = async ( ) => {
await registerPush ( ) ;
onDismiss?. ( ) ;
} ;
return (
< View style= { [ styles. container, { backgroundColor: colors. surface } ] } >
< View style= { [ styles. iconContainer, { backgroundColor: colors. primaryLight } ] } >
< Bell size= { 32 } color= { colors. primary} / >
< / View>
< Text style= { [ styles. title, { color: colors. text } ] } >
Mantente al día
< / Text>
< Text style= { [ styles. description, { color: colors. textSecondary } ] } >
Activa las notificaciones para recibir actualizaciones importantes
< / Text>
< View style= { styles. buttons} >
< Button
title= "Activar"
onPress= { handleEnable}
loading= { isRegistering}
/ >
< Button
title= "Ahora no"
variant= "ghost"
onPress= { onDismiss}
/ >
< / View>
< / View>
) ;
}
const styles = StyleSheet. create ( {
container: {
padding: 24 ,
borderRadius: 16 ,
alignItems: 'center' ,
margin: 16 ,
} ,
iconContainer: {
width: 64 ,
height: 64 ,
borderRadius: 32 ,
alignItems: 'center' ,
justifyContent: 'center' ,
marginBottom: 16 ,
} ,
title: {
fontSize: 20 ,
fontWeight: '600' as const ,
marginBottom: 8 ,
} ,
description: {
fontSize: 14 ,
textAlign: 'center' ,
marginBottom: 24 ,
lineHeight: 20 ,
} ,
buttons: {
width: '100%' ,
gap: 8 ,
} ,
} ) ;
Deep Linking desde Notificaciones
Configurar Deep Links
import { useEffect } from 'react' ;
import * as Notifications from 'expo-notifications' ;
import { useRouter } from 'expo-router' ;
function useNotificationDeepLink ( ) {
const router = useRouter ( ) ;
useEffect ( ( ) => {
const subscription = Notifications. addNotificationResponseReceivedListener (
( response) => {
const data = response. notification. request. content. data;
if ( data?. type === 'message' ) {
router. push ( ` /(tabs)/messages/ ${ data. chatId} ` ) ;
} else if ( data?. type === 'item' ) {
router. push ( {
pathname: '/(tabs)/(home)/item-detail' ,
params: { id: data. itemId } ,
} ) ;
} else if ( data?. route) {
router. push ( data. route as string ) ;
}
}
) ;
return ( ) => subscription. remove ( ) ;
} , [ router] ) ;
}
Enviar con Deep Link Data
await Notifications. scheduleNotificationAsync ( {
content: {
title: 'Nuevo mensaje' ,
body: 'Juan te envió un mensaje' ,
data: {
type: 'message' ,
chatId: 'chat-123' ,
} ,
} ,
trigger: null ,
} ) ;
Checklist de Implementación