Skip to main content

Overview

The notification system provides real-time alerts for reservation reminders, payment confirmations, and system updates. Notifications use Expo’s push notification service with automatic token management.

Notification Setup

Permission Requests

The app requests notification permissions on startup:
import * as Notifications from 'expo-notifications';

useEffect(() => {
  const pedirPermisos = async () => {
    const { status } = await Notifications.getPermissionsAsync();
    if (status !== 'granted') {
      await Notifications.requestPermissionsAsync();
    }
  };
  pedirPermisos();
}, []);
Permissions are required for both local and push notifications. Users can revoke these in system settings.

Notification Handler

Configure how notifications are displayed:
Notifications.setNotificationHandler({
  handleNotification: async () => ({
    shouldShowAlert: true,
    shouldPlaySound: true,
    shouldSetBadge: false,
    shouldShowBanner: true,
    shouldShowList: true,
  }),
});
Display notification when app is in foreground
Play notification sound
Update app icon badge (disabled to avoid clutter)
Show banner at top of screen
Add to notification center

Push Token Registration

Generating Tokens

async function registerForPushNotificationsAsync() {
  let token;

  // Configure Android notification channel
  if (Platform.OS === 'android') {
    await Notifications.setNotificationChannelAsync('default', {
      name: 'default',
      importance: Notifications.AndroidImportance.MAX,
      vibrationPattern: [0, 250, 250, 250],
      lightColor: '#FF231F7C',
    });
  }

  // Check device compatibility
  if (Device.isDevice) {
    const { status: existingStatus } = await Notifications.getPermissionsAsync();
    let finalStatus = existingStatus;

    if (existingStatus !== 'granted') {
      const { status } = await Notifications.requestPermissionsAsync();
      finalStatus = status;
    }

    if (finalStatus !== 'granted') {
      alert('¡Se necesitan permisos para recibir notificaciones!');
      return;
    }

    // Get project ID from config
    const projectId = 
      Constants?.expoConfig?.extra?.eas?.projectId ?? 
      Constants?.easConfig?.projectId;

    try {
      const pushTokenString = (
        await Notifications.getExpoPushTokenAsync({ projectId })
      ).data;
      
      console.log("✅ TOKEN GENERATED:", pushTokenString);
      token = pushTokenString;
    } catch (e: unknown) {
      alert(`Error generating token: ${e}`);
    }
  } else {
    alert('Debes usar un dispositivo físico para las notificaciones Push');
  }

  return token;
}
Push notifications require a physical device. They will not work in iOS Simulator or Android Emulator.

Saving Tokens to Firebase

Tokens are automatically saved when users authenticate:
export const usePushNotifications = () => {
  const [expoPushToken, setExpoPushToken] = useState<string | undefined>('');

  useEffect(() => {
    registerForPushNotificationsAsync().then((token) => {
      setExpoPushToken(token);

      // Listen for auth state changes
      const unsubscribeAuth = onAuthStateChanged(auth, async (user) => {
        if (user && token) {
          try {
            const userRef = doc(db, 'users', user.uid);
            await setDoc(
              userRef, 
              { pushToken: token }, 
              { merge: true }
            );
            console.log("💾 TOKEN SAVED TO FIREBASE FOR:", user.email);
          } catch (error) {
            console.error("❌ Error saving token:", error);
          }
        }
      });

      return () => unsubscribeAuth();
    });
  }, []);

  return { expoPushToken };
};
Using merge: true ensures other user data isn’t overwritten when saving the token.

Scheduled Notifications

Reservation Reminders

Users receive reminders 15 minutes before their reservation:
const programarRecordatorio = async (fechaInicio: Date) => {
  // Calculate trigger time (15 minutes before)
  const triggerDate = new Date(fechaInicio.getTime() - 15 * 60 * 1000);
  const now = new Date();

  // Calculate seconds until trigger
  const diffInSeconds = Math.floor((triggerDate.getTime() - now.getTime()) / 1000);

  if (diffInSeconds <= 0) {
    console.log("Reservation too soon, no reminder scheduled.");
    return;
  }

  try {
    await Notifications.scheduleNotificationAsync({
      content: {
        title: "⏳ Tu reserva está por iniciar",
        body: `Faltan 15 min para tu reserva en el cajón ${selectedSpot}. ¡Prepara tu código QR!`,
        sound: true,
        data: { url: '/mis-reservas' },
      },
      trigger: {
        type: 'timeInterval',
        seconds: diffInSeconds,
        repeats: false,
      } as any,
    });
    
    console.log(`🔔 Notification scheduled in ${diffInSeconds} seconds.`);
  } catch (error) {
    console.log("Error scheduling notification:", error);
  }
};
Scheduled notifications are stored locally and trigger even if the app is closed.

Notification Data

Notifications can include navigation data:
data: { url: '/mis-reservas' }
This allows deep linking when users tap the notification:
responseListener.current = Notifications.addNotificationResponseReceivedListener(
  (response) => {
    const url = response.notification.request.content.data.url;
    if (url) {
      router.push(url);
    }
  }
);

Notification Listeners

Foreground Notifications

Handle notifications when app is open:
notificationListener.current = Notifications.addNotificationReceivedListener(
  (notification) => {
    setNotification(notification);
    console.log("📬 Notification received:", notification);
  }
);

Tap Responses

Handle user taps on notifications:
responseListener.current = Notifications.addNotificationResponseReceivedListener(
  (response) => {
    console.log("👆 User tapped notification:", response);
    
    // Navigate or perform action
    const data = response.notification.request.content.data;
    if (data.url) {
      router.push(data.url);
    }
  }
);

Cleanup

Always remove listeners when component unmounts:
return () => {
  notificationListener.current && 
    notificationListener.current.remove();
  responseListener.current && 
    responseListener.current.remove();
};

Android Configuration

Notification Channels

Android requires notification channels:
if (Platform.OS === 'android') {
  await Notifications.setNotificationChannelAsync('default', {
    name: 'default',
    importance: Notifications.AndroidImportance.MAX,
    vibrationPattern: [0, 250, 250, 250],
    lightColor: '#FF231F7C',
  });
}

Importance Levels

LevelBehavior
MINNo sound, no popup
LOWSound but no popup
DEFAULTSound and popup
HIGHSound, popup, heads-up
MAXSound, popup, heads-up, interruption

Notification Types

The app sends various notification types:
Trigger: 15 minutes before reservation start
Content: Spot number, time remaining, QR code prompt
Type: Local scheduled notification
Trigger: After successful payment
Content: Credit amount, transaction ID
Type: Push notification from server
Trigger: Reservation activated, completed, or canceled
Content: Status change, penalty amount (if applicable)
Type: Push notification from server
Trigger: Achievement criteria met
Content: Achievement name, XP earned
Type: Local immediate notification
Trigger: New friend request or acceptance
Content: Friend’s name and profile
Type: Push notification from server

Server-Side Notifications

Push tokens enable server-to-user notifications:
// Server sends notification via Expo Push API
POST https://exp.host/--/api/v2/push/send
{
  "to": "ExponentPushToken[xxxxxxxxxxxxxx]",
  "sound": "default",
  "title": "Payment Received",
  "body": "Your wallet has been credited with 300 credits",
  "data": { "url": "/recargarsaldo" }
}
The server retrieves push tokens from Firestore user documents to send targeted notifications.

Testing Notifications

Local Testing

Test scheduled notifications:
// Schedule test notification in 5 seconds
await Notifications.scheduleNotificationAsync({
  content: {
    title: "Test Notification",
    body: "This is a test",
  },
  trigger: {
    seconds: 5,
  },
});

Push Testing

Use Expo’s push notification tool:
https://expo.dev/notifications
Enter your push token and send test notifications.

Best Practices

Request permissions at a meaningful moment, not immediately on app launch. Consider requesting after first successful action.
Tokens can change. Always update Firebase when a new token is generated:
onAuthStateChanged(auth, (user) => {
  if (user && token) {
    updateDoc(doc(db, 'users', user.uid), { pushToken: token });
  }
});
Group related notifications to avoid spam:
// Instead of 3 separate notifications
"Reservation in 15 min"
"Reservation in 10 min"
"Reservation in 5 min"

// Send only one
"Reservation in 15 min"

Troubleshooting

No notifications received?
  1. Verify permissions are granted
  2. Check push token is saved to Firebase
  3. Ensure app is on a physical device
  4. Verify notification handler is configured
  5. Check Android notification channel settings

Debug Logging

Enable verbose logging to diagnose issues:
console.log("Token:", expoPushToken);
console.log("Notification:", notification);
console.log("Response:", response);

Build docs developers (and LLMs) love