RORK LABJP
TOOLING — Rork's developer repos keep moving: rork-xcode was updated on July 16, rork-device on July 15, and rork-plist on July 13OPUS46 — Claude Opus 4.6 is live in Rork, and Rork Max is built to assemble apps on top of Claude CodeSIM — A cloud iOS simulator runs in the browser, with one click to install on a device and two clicks to publish to the App StoreMAX — Rork Max emits pure Swift rather than React Native, reaching iPhone, iPad, Apple Watch, Apple TV, Vision Pro, and even iMessageNATIVE — That opens up HealthKit, ARKit and LiDAR, NFC, Dynamic Island, Live Activities, 3D through Metal, and on-device inference with Core MLSEED — Rork raised a $15M seed led by Left Lane Capital, with Peak XV and a16z Speedrun joining the roundTOOLING — Rork's developer repos keep moving: rork-xcode was updated on July 16, rork-device on July 15, and rork-plist on July 13OPUS46 — Claude Opus 4.6 is live in Rork, and Rork Max is built to assemble apps on top of Claude CodeSIM — A cloud iOS simulator runs in the browser, with one click to install on a device and two clicks to publish to the App StoreMAX — Rork Max emits pure Swift rather than React Native, reaching iPhone, iPad, Apple Watch, Apple TV, Vision Pro, and even iMessageNATIVE — That opens up HealthKit, ARKit and LiDAR, NFC, Dynamic Island, Live Activities, 3D through Metal, and on-device inference with Core MLSEED — Rork raised a $15M seed led by Left Lane Capital, with Peak XV and a16z Speedrun joining the round
Articles/Dev Tools
Dev Tools/2026-05-26Intermediate

iOS Foreground Notifications Disappear in Expo — A Practical Fix Guide

A focused walkthrough of why iOS silently drops push notifications when the app is in the foreground, and how to wire setNotificationHandler correctly for iOS 14+ with expo-notifications.

expo-notifications7iOS109push-notificationtroubleshooting65Rork515

"Test pushes show up when the app is in the background, but the moment it is in the foreground they vanish without a sound." I ran into this myself while running an A/B notification test for one of my healing apps, and it always traces back to the same spot: iOS treats foreground notifications as opt-in, and your app has to ask for them explicitly.

I have been shipping indie apps since 2014 with around fifty million downloads across the lineup, and the expo-notifications surface area shifts quietly between SDK versions. Every time I upgrade, this is the trap I rediscover. The fix is small once you see it. Let me walk you through how to confirm the cause and what the cleanest code shape looks like today.

Symptoms and how to confirm the cause

Run this quick triage to be certain:

  • Send a push while the app is in the background — the banner appears.
  • Send the same push with the app open — nothing shows, no sound.
  • Open Notification Center — the entry is logged in history.

If those three match, the problem is almost always a missing foreground handler or the outdated iOS 13 display flags. It is not a token, APNs, or FCM problem. Only when the notification is missing from history should you look at certificates, FCM Server Keys, or the scope passed to getExpoPushTokenAsync.

Cause 1: setNotificationHandler is never called

iOS drops foreground notifications by default. expo-notifications lets you opt in through setNotificationHandler, which has to run before the first render.

// app/_layout.tsx or the topmost entry point
import * as Notifications from "expo-notifications";
 
Notifications.setNotificationHandler({
  handleNotification: async () => ({
    shouldShowBanner: true,
    shouldShowList: true,
    shouldPlaySound: true,
    shouldSetBadge: true,
  }),
});

Place it at the top of your root layout or just before registerRootComponent. Wrapping it in useEffect is risky because a push that arrives during cold start may slip through before the effect fires.

Cause 2: Still using shouldShowAlert (deprecated on iOS 14+)

A lot of samples online still look like this:

// Old API — iOS 14+ logs a deprecation warning and the new keys are recommended
handleNotification: async () => ({
  shouldShowAlert: true,
  shouldPlaySound: true,
  shouldSetBadge: true,
}),

iOS 14 split notification surfaces into banner (top of screen) and list (Notification Center, lock screen), so a single shouldShowAlert can no longer control both. From expo-notifications v0.27 onward, shouldShowBanner and shouldShowList are the recommended pair. Older versions silently fall back to the legacy keys, which masks the issue but bites you the moment you upgrade.

Cause 3: Permission is stuck in provisional

A common mistake is trusting only the result of getPermissionsAsync() without ever calling requestPermissionsAsync(). iOS can return a provisional status on first install — quiet notifications are allowed, but no foreground banner.

import * as Notifications from "expo-notifications";
 
async function ensureNotificationPermission() {
  const current = await Notifications.getPermissionsAsync();
  // provisional reports granted too, so we have to inspect ios.status
  if (
    current.status === "granted" &&
    current.ios?.status === Notifications.IosAuthorizationStatus.AUTHORIZED
  ) {
    return true;
  }
 
  const requested = await Notifications.requestPermissionsAsync({
    ios: {
      allowAlert: true,
      allowSound: true,
      allowBadge: true,
      allowDisplayInCarPlay: false,
      allowAnnouncements: false,
    },
  });
  return requested.status === "granted";
}

Checking ios.status is the critical step. Android tells you everything through granted, but iOS exposes a finer grain — distinguishing AUTHORIZED from EPHEMERAL from PROVISIONAL actually matters here.

Cause 4: The payload is too thin to render

If you return shouldShowBanner: true but the server sends a payload with no title or empty body, iOS quietly drops the visual. The minimum I keep in production looks like this:

{
  "to": "ExponentPushToken[xxxxxxxx]",
  "title": "New wallpapers added",
  "body": "Tap to see today's pick",
  "sound": "default",
  "badge": 1,
  "_displayInForeground": true
}

_displayInForeground is an Expo-specific flag and is redundant once setNotificationHandler is wired up, but it keeps mixed-SDK environments predictable. I leave it in place for older clients that have not been rebuilt yet.

Cause 5: A Focus mode is filtering you out

On iOS 15 and later, if your app is not in the "Allowed Apps" list for the user's active Focus, foreground notifications are silenced too. Check Settings → Focus → (mode) → Allowed Apps on your test device. Users who switch between several Focus profiles often forget to add new apps to each one.

This is not strictly a developer-side bug, but the same support questions land in your inbox eventually. Keep this as the first triage question for end-user reports.

A minimal reproduction snippet

Here is everything stitched together as a screen that fires a local notification three seconds after a tap. It removes the server from the loop entirely.

import * as Notifications from "expo-notifications";
import { Button, View } from "react-native";
 
Notifications.setNotificationHandler({
  handleNotification: async () => ({
    shouldShowBanner: true,
    shouldShowList: true,
    shouldPlaySound: true,
    shouldSetBadge: true,
  }),
});
 
export default function DebugScreen() {
  return (
    <View>
      <Button
        title="Fire local push in 3s"
        onPress={async () => {
          const perm = await Notifications.requestPermissionsAsync();
          if (perm.status !== "granted") return;
          await Notifications.scheduleNotificationAsync({
            content: { title: "Test", body: "Foreground check", sound: "default" },
            trigger: { seconds: 3 },
          });
        }}
      />
    </View>
  );
}

If this does not produce a banner, the cause is upstream of the handler — permissions or Focus mode rather than the display flags.

Preventive checklist

  • Call setNotificationHandler at the top of your root layout.
  • Use the iOS 14+ keys (shouldShowBanner and shouldShowList) only.
  • Inspect ios.status instead of trusting granted alone.
  • Hide a "fire local push" button somewhere in your settings screen so you can triage user reports inside minutes.

For my own wallpaper apps, the moment a release-only notification bug shows up I drop this debug screen into TestFlight and watch the first few hours of telemetry. Knowing whether the problem lives in the app, the payload, or the device almost always shortens the loop to a single day.

Thanks for reading — I hope this saves you a few hours of the same investigation.

Share

Thank You for Reading

Rork Lab is ad-free, supported entirely by members like you. We publish practical guides daily with implementation code, benchmarks, and production-ready patterns. If you've found it useful, we'd love to have you on board.

  • Copy-paste ready implementation code
  • New advanced guides published daily
  • $5/mo or $10 for lifetime access
View Membership →

If you found this article helpful, a small tip ($1.50) would mean a lot to us. Your support helps keep this site ad-free and covers server and hosting costs.

Related Articles

Dev Tools2026-06-22
Your Daily Reminder Stops Firing After a Couple of Weeks — iOS's Invisible 64-Notification Cap
When a daily reminder built with Rork (Expo) goes silent after a while, the cause is usually iOS's 64 pending-notification limit. Design a repeating calendar trigger for fixed messages and a rolling reschedule for daily-changing content, with working code that survives DST and multiple reminders.
Dev Tools2026-05-07
Rork iOS App: Why Your App Tracking Transparency Prompt Never Shows Up — and How to Fix It
Three real causes the ATT (App Tracking Transparency) dialog never appears in Rork-generated iOS apps — Info.plist, call timing, and AdMob init order — with working code and on-device verification steps.
Dev Tools2026-05-03
When 'pod install' Fails on Rork-Exported iOS Projects — A Practical Guide for the Apple Silicon Era
Most pod install failures on Rork-generated iOS projects come from the same handful of root causes — Apple Silicon ffi mismatches, Ruby version conflicts, CocoaPods version drift, Xcode path issues, and Hermes/Folly compile errors. Here is the order I work through them.
📚RECOMMENDED BOOKS
Build a Large Language Model (From Scratch)
Sebastian Raschka
LLM Dev
Prompt Engineering for LLMs
Berryman & Ziegler
Prompting
AI Engineering
Chip Huyen
AI Eng
* Contains affiliate links
See all →