Table of Contents

    Book an Appointment

    Why Do Expo Push Notifications Arrive Silently on iOS?

    While working on a dedicated logistics management platform, real-time alerting was a critical business requirement. Drivers and dispatchers relied on immediate auditory cues and visual badge updates to process time-sensitive delivery assignments. We chose React Native and Expo to build a unified mobile experience, leveraging Expo Application Services (EAS) for streamlined deployment.

    During the UAT phase on TestFlight, a severe discrepancy surfaced. While Android devices processed alerts flawlessly with full sound and vibration, iOS devices received the notifications silently. The alerts appeared in the notification center, but there was no sound and the app icon badge count remained stubbornly at zero. Missing a dispatch alert meant delayed deliveries, making this a critical production blocker.

    This scenario highlights a common architectural misunderstanding between client-side listeners and operating system-level background processes. We are sharing this technical deep dive so that engineering leaders and teams can avoid this frustrating pitfall when implementing expo push notifications.

    Where Did the React Native Push Notifications Issue Emerge?

    The problem isolated itself exclusively to the iOS ecosystem. The architecture relied on the standard expo-notifications package. The application was compiled using an EAS production build and distributed via TestFlight. APNs credentials and keys were fully configured in the Apple Developer Portal and correctly linked in the EAS project settings.

    To eliminate our backend infrastructure as the culprit, we bypassed our Node.js microservices and tested the delivery directly using the Expo Push Notification testing tool. The symptoms were consistent:

    • The notification successfully traversed the Apple Push Notification service (APNs) and arrived on the iPhone.
    • The visual banner was displayed.
    • No notification sound played, despite the iPhone volume being maxed out and hardware switches set to ring.
    • The app icon badge count did not increment.
    • iOS permissions were explicitly granted for alerts, sounds and badges.

    Because the push tokens were valid and the messages were arriving, the communication bridge was intact. The failure lay in how iOS interpreted the notification payload when the app was backgrounded or killed.

    What Caused the iOS App Icon Badge and Sound to Fail?

    Our initial investigation naturally gravitated toward the client-side React Native codebase. Our implementation looked mathematically correct based on standard documentation. We had configured the notification handler precisely to enforce sound and badges:

    import * as Notifications from 'expo-notifications';
    Notifications.setNotificationHandler({
      handleNotification: async () => ({
        shouldPlaySound: true,
        shouldSetBadge: true,
        shouldShowBanner: true,
        shouldShowList: true,
      }),
    });
    

    We had also correctly implemented the platform-specific channel logic for Android, which explained why Android was functioning perfectly:

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

    The oversight was an architectural one: Notifications.setNotificationHandler() only dictates behavior when the application is actively in the foreground.

    When an iOS application is in the background or fully terminated, the React Native JavaScript thread is not running. Therefore, iOS entirely ignores the client-side `setNotificationHandler` configuration. Instead, the iOS operating system reads the incoming APNs JSON payload directly. If the payload does not explicitly command the OS to play a sound or set a badge, iOS defaults to a silent delivery with no badge modification.

    How Did We Diagnose the Expo Push Notifications Failure?

    When addressing mobile architecture bugs, we methodically eliminate variables. We considered several solutions and potential root causes.

    Did We Need to Fix APNs Certificates?

    Our first thought was a potential mismatch in EAS credentials. We re-verified our p8 keys and bundle identifiers. However, because the notification text was successfully appearing on the lock screen, we concluded that the cryptographic handshake between Expo, APNs and the device was perfectly intact. Certificate errors result in zero delivery, not silent delivery.

    Was the React Native Client Code Incorrect?

    We audited our `addNotificationReceivedListener` and `addNotificationResponseReceivedListener` methods. While these listeners successfully refreshed the internal unread count when a user tapped the notification, they could not retroactively trigger an iOS system sound or update the OS-level badge after the notification had already been delivered to a killed app.

    Was the Server Payload the Missing Link?

    We realized that when using the Expo web testing tool, entering only the “Title” and “Body” generates a minimal JSON payload. Android’s FCM (Firebase Cloud Messaging) defaults to playing the default system sound when it receives a standard notification block. Apple’s APNs, however, requires explicit flags. The root cause was that our test payloads—and subsequently our backend payloads—were omitting the mandatory iOS-specific properties.

    How Did We Finally Fix the React Native Push Notifications Payload?

    To resolve the issue, we had to shift our focus from the client-side React Native code to the server-side payload generation (and the advanced JSON input on the Expo test tool). To instruct the iOS system to play a sound and update the badge while the app is inactive, the payload sent to the Expo Push API must contain specific fields.

    Here is the corrected JSON payload structure that we implemented in our backend service. If you are testing via the Expo Push tool, you must include these variables in the extra JSON data section:

    {
      "to": "ExponentPushToken[xxxxxxxxxxxxxxxxxxxxxx]",
      "title": "New Dispatch Assignment",
      "body": "You have a new delivery routed to your queue.",
      "sound": "default", 
      "badge": 1,
      "data": {
        "screen": "DispatchDetails",
        "params": { "dispatchId": "12345" }
      }
    }
    

    Key Technical Corrections:

    • “sound”: “default” – This single line is critical for iOS. Without it, the notification is delivered silently. You can also specify a custom sound file name if you have bundled a custom `.wav` file into your Xcode project via Expo plugins.
    • “badge”: 1 – This explicitly tells the iOS SpringBoard to display a red badge with the number ‘1’. Note that this is an absolute integer, not an incrementer. Your backend must calculate the user’s unread count and send the accurate total number.

    On the client side, to clear the badge when the user opens the app, we updated our React Native initialization logic:

    import * as Notifications from 'expo-notifications';
    import { useEffect } from 'react';
    import { AppState } from 'react-native';
    export function useBadgeManager() {
      useEffect(() => {
        const subscription = AppState.addEventListener('change', nextAppState => {
          if (nextAppState === 'active') {
            // Clear the badge on iOS when app is opened
            Notifications.setBadgeCountAsync(0);
          }
        });
        return () => {
          subscription.remove();
        };
      }, []);
    }
    

    What Are the Key Lessons for Mobile Engineering Teams?

    Complex cross-platform behavior is exactly why enterprise companies hire react native developers for cross-platform apps rather than relying on generic solutions. Here are the actionable takeaways from this deployment:

    • Foreground vs. Background States: Never assume client-side configuration handles background execution. `setNotificationHandler` is strictly for foregrounded apps. Operating systems dictate background behavior based on the raw push payload.
    • Platform Defaults Differ: Android FCM and iOS APNs have different default behaviors. Android is generally more aggressive with sounds and vibrations by default, whereas Apple enforces strict payload requirements for auditory interruptions.
    • Absolute vs. Relative Badging: iOS does not support “increment badge by 1” via push payload. Your backend database must act as the source of truth for the unread count and explicitly send that integer in the `badge` field.
    • EAS Production Testing: Testing in Expo Go masks real-world APNs behavior. Always test push notifications using an EAS production or internal distribution build via TestFlight to replicate the exact production environment.
    • Clearing Badges Programmatically: Always implement an `AppState` listener to reset the badge count (`setBadgeCountAsync(0)`) when the user interacts with the app; otherwise, the red dot will remain indefinitely.
    • Specialized Capabilities: Debugging device-specific OS behaviors requires deep native experience. When organizations scale, they often hire expo developers for mobile solutions who understand the native bridges beneath the JavaScript layer.

    How Can You Avoid Similar Mobile Architecture Pitfalls?

    What seemed like a React Native bug was actually an infrastructural discrepancy in how iOS and Android parse push payloads. By adjusting the server-side payload to explicitly include sound and badge parameters, we restored full auditory and visual alerting to the iOS application, ensuring the logistics platform could operate in real-time.

    Modern mobile development requires a deep understanding of native operating systems, beyond just writing UI components. If your organization is struggling with cross-platform inconsistencies, performance bottlenecks or complex EAS build pipelines, it might be time to scale your engineering capabilities. Whether you need to augment your team or hire software developer resources with specialized mobile architecture expertise, structured delivery practices ensure predictable outcomes. Feel free to contact us to discuss your engineering needs.

    Social Hashtags

    #ExpoNotifications #ReactNative #Expo #iOSDevelopment #PushNotifications #ReactNativeDevelopment #ExpoEAS #APNs #JavaScript #MobileAppDevelopment #iOSPushNotifications #ExpoPushNotifications #ReactNativeTips #MobileAppDebugging #CrossPlatformDevelopment

    Frequently Asked Questions