npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

expo-background-refresh-status

v1.0.0

Published

Real iOS Background App Refresh status (UIApplication.backgroundRefreshStatus), with a change listener. Fills the gap where expo-background-task hardcodes its iOS status.

Readme

expo-background-refresh-status

Reads the real iOS Background App Refresh setting — UIApplication.backgroundRefreshStatus — and tells you when it changes.

npm license

Why this exists

If you use expo-background-task, you might reasonably expect getStatusAsync() to tell you whether the user has Background App Refresh switched on. On iOS, it does not. Its status is:

// expo-background-task/ios/BackgroundTaskScheduler.swift
public static func supportsBackgroundTasks() -> Bool {
#if targetEnvironment(simulator)
  return false
#else
  return true
#endif
}

That value is fed straight into getStatusAsync(). It returns restricted on the Simulator and available on every physical device, unconditionally — it never reads the OS permission at all. So a "background refresh is off" warning built on it can never fire on a shipping build, no matter what the user has actually set in Settings → General → Background App Refresh.

(The predecessor library, expo-background-fetch, did read the real status. The check appears to have been dropped, likely unintentionally, in the rewrite to a BGTaskScheduler-based implementation.)

This package does the read properly, and adds the change notification that expo-background-task doesn't surface either.

What you get

| | expo-background-task (iOS) | this package | | ------------------------ | ------------------------------- | ------------------------------------------ | | Reads the real OS status | ❌ hardcoded per build target | ✅ UIApplication.backgroundRefreshStatus | | Status values | available | restricted | available | denied | restricted | | Change notification | ❌ (consumers poll on AppState) | ✅ addStatusChangeListener |

The three values mirror Apple's UIBackgroundRefreshStatus exactly.

Installation

npx expo install expo-background-refresh-status

No config plugin, no Info.plist keys, no entitlements. Autolinking picks it up. It is native code, so it requires a development build or a store build — it does not work in Expo Go.

Requirements

Expo SDK 54 or newer. Two things set that floor: expo-module.config.json uses the apple platform key, which autolinking only understands from SDK 51 on, and the JS entry point imports requireOptionalNativeModule from expo. On an older SDK the install succeeds and the module is simply never linked — isAvailable is false with nothing in the build log to explain it. The expo peer range is >=54, so npx expo install will warn you rather than let that happen quietly.

Usage

import {
  addStatusChangeListener,
  getStatusAsync,
  isAvailable,
} from 'expo-background-refresh-status';

const status = await getStatusAsync();
// 'available' | 'denied' | 'restricted' | null

if (status !== null && status !== 'available') {
  // Background refresh won't run. Consider prompting the user.
}

const sub = addStatusChangeListener(({ status }) => {
  console.log('background refresh is now', status);
});
// later
sub.remove();

API

getStatusAsync(): Promise<BackgroundRefreshStatus | null>

The current setting.

  • 'available' — Background App Refresh is on for this app.
  • 'denied' — the user switched it off, either for this app specifically or system-wide. Apple reports both as the same case.
  • 'restricted' — parental controls or MDM forbid it; the user cannot change it.
  • null — no reading was possible (see isAvailable).

Check status !== 'available', not status === 'restricted'. Both non-available cases mean background refresh won't run, and denied is by far the common one.

null means "no reading", not "available". Leave whatever state you had unchanged rather than treating it as permission granted.

addStatusChangeListener(listener): EventSubscription

Subscribes to UIApplication.backgroundRefreshStatusDidChangeNotification (iOS 7+). The listener receives { status }. Returns an inert subscription when isAvailable is false.

EventSubscription is { remove(): void } and is exported from this package. It is deliberately not imported from expo-modules-core: doing that would require declaring expo-modules-core as a peer dependency, which makes expo-doctor report a missing peer in every consuming app and forces you to add a direct dependency on something you already get through expo. The real subscription satisfies this type structurally, so nothing is lost.

isAvailable: boolean

Whether the native module is present in this binary. false on Android, on web, and in Expo Go. Both functions above fail soft when it's false — check this first if you'd rather fail loudly.

Platform support

Apple platforms only. There is deliberately no Android implementation: Android has no equivalent per-app background-refresh toggle to read.

The podspec declares iOS, tvOS and visionOS, because Apple documents both APIs on all three (iOS 7+, tvOS 11+, visionOS 1+) and because an undeclared platform is skipped by the same silent autolinking filter described below. Only iOS is verified on hardware.

On Android and web, importing this package is safe — it resolves to null internally, isAvailable is false, getStatusAsync() resolves null, and listeners are inert. Nothing throws at import time, so you don't need to guard the import itself.

Three deliberate behaviours worth knowing

  • An unknown future enum case maps to 'available'. If Apple adds a fourth UIBackgroundRefreshStatus case, this returns 'available' rather than inventing a warning. A hint that fabricates itself is worse than one that stays quiet.
  • 'restricted' ships unverified. It requires a supervised device (parental controls / MDM), which isn't reachable in ordinary testing. The mapping is a one-line switch case and is correct by inspection, but it has not been exercised on a device. This is a further reason to branch on !== 'available' rather than per value.
  • Low Power Mode does not change the status. Verified on device: iOS keeps reporting 'available' under Low Power Mode even though it suppresses background execution. So 'available' means "not switched off", not "background work will run" — treat any metric built on it as a ceiling. It also means you should probably not warn the user about Low Power Mode from this API: the switch they'd be sent to is genuinely still on.

Verified on device

getStatusAsync() (available and denied), addStatusChangeListener() and isAvailable are all confirmed on a physical iPhone against an Expo SDK 54 app, as of 1.0.0. restricted is the sole exception, per the note above.

SDK 54 is the oldest supported target, which is why it's the one verified by hand; the example/ app in this repo tracks the current SDK instead, so both ends of the supported range get exercised. The fail-soft path — everything isAvailable === false implies — is covered by unit tests rather than by hand, since it's the branch an iPhone can't reach.

Deployment target, if you're on an older SDK: this package's podspec targets iOS 15.1 to match Expo SDK 54 and every first-party expo-* module. If your app's deployment target is lower than a module's podspec floor, Expo's CocoaPods autolinking silently skips that module — the build succeeds, no pod is compiled, nothing appears in the log, and isAvailable is simply false at runtime. (expo-modules-autolinking resolve still reports the module as resolved, because that filter only runs during pod install.) 0.1.0 shipped with a 16.4 floor and was inert on SDK 54 apps for exactly this reason; if you're on 0.1.0, upgrade.

Contributing

Issues and PRs welcome. The native surface is one Swift file (ios/BackgroundRefreshStatusModule.swift) — it is meant to stay that small.

See also

  • expo/expo issue tracker — the upstream fix for expo-background-task's hardcoded status is the long-term home for the read. Even if it lands, the collapsed two-value enum and the missing change notification would remain.

License

MIT © Arnau Tresserras