Skip to content

Recipe: foreground → background location

Problem. Your app needs background location (running tracker, ride-share driver mode, etc.), but both iOS and Android require you to request the foreground permission first and only then ask for the always/background permission. The native APIs expose both as separate permissions, and getting the order wrong throws a confusing OS error.

Solution. Use Permissions.BUNDLES.LOCATION_BACKGROUND. On Android it expands to [ACCESS_FINE_LOCATION, ACCESS_BACKGROUND_LOCATION] — two independent runtime permissions that must be requested in order. On iOS it expands to just [LOCATION_WHEN_IN_USE] because iOS models Core Location as a single authorization: you request "When In Use" first, then upgrade to "Always" as a follow-up step on the same permission rather than as a separate request. Feed the bundle into a sequential useMultiplePermissions call and the flow Just Works on both platforms — on iOS you'll see one prompt, on Android two.

What you'll use

Code

tsx
import React from "react";
import { Button, Text, View } from "react-native";
import { useMultiplePermissions } from "react-native-permission-handler";
import { Permissions } from "react-native-permission-handler/rnp";

// iOS: ["LOCATION_WHEN_IN_USE"] — one entry.
// Android: ["ACCESS_FINE_LOCATION", "ACCESS_BACKGROUND_LOCATION"] — two entries.
const LOCATION_BUNDLE = Permissions.BUNDLES.LOCATION_BACKGROUND;

const entries = LOCATION_BUNDLE.map((permission, index) => {
  const isBackground = index === 1; // only exists on Android
  return isBackground
    ? {
        id: "location-background",
        permission,
        prePrompt: {
          title: "Always allow location",
          message:
            "We need background location so your run keeps recording when the screen is off.",
        },
        blockedPrompt: {
          title: "Background location blocked",
          message: "Switch location to 'Always' in Settings for background tracking.",
        },
      }
    : {
        id: "location-foreground",
        permission,
        prePrompt: {
          title: "Location while using the app",
          message: "We need your location to track your run in real time.",
        },
        blockedPrompt: {
          title: "Location blocked",
          message: "Enable location access in Settings to continue.",
        },
      };
});

export function LiveTrackingSetup() {
  const perms = useMultiplePermissions({
    strategy: "sequential",
    permissions: entries,
    onAllGranted: () => startTracking(),
  });

  if (perms.allGranted) return <Text>Tracking…</Text>;

  return (
    <View>
      <Text>Enable location to start tracking your run.</Text>
      <Button title="Enable location" onPress={perms.request} />
      {perms.blockedPermissions.length > 0 && (
        <Text>Resolve the blocked permissions above, then tap again.</Text>
      )}
    </View>
  );
}

function startTracking() {
  // ...
}

Why sequential, not parallel

On both iOS and Android, requesting background location before foreground is granted either no-ops or outright fails. Sequential enforces the correct order: the background entry won't be prompted until the foreground entry returns granted. If the user denies or dismisses the foreground prompt, the flow stops — tap the "Enable location" button again to resume from the beginning, or call perms.resume() to continue from the current ungranted step.

iOS "Always" upgrades

On iOS, granting "When In Use" is the whole flow that this bundle can drive today. iOS does not model "Always" as a separately requestable permission — the system expects you to call requestAlwaysAuthorization as a second step on the already-granted permission, and the OS only shows the "Always Allow" dialog automatically the second time a background location event fires in-app, not on demand from your UI. There is no in-app API to force the prompt.

The practical recovery path is to send the user to Settings and let them flip "When Using the App" to "Always." As of v0.8.0, engine.openSettings("location") deep-links into the iOS Location Services sub-page, which makes this one tap away from the app. The library automatically passes the hook's permission identifier to the engine, so calling handler.openSettings() from a button handler does the right thing on both platforms.

tsx
function AlwaysAllowUpgradeButton() {
  const location = usePermissionHandler({
    permission: Permissions.LOCATION_WHEN_IN_USE,
    prePrompt: {
      title: "Location while using the app",
      message: "We need foreground location first.",
    },
    blockedPrompt: {
      title: "Location blocked",
      message: "Enable location in Settings.",
    },
  });

  if (!location.isGranted) return null;

  return (
    <View>
      <Text>Tracking works while the app is open. For background tracking,</Text>
      <Text>switch location access to "Always" in Settings.</Text>
      <Button title="Open Location settings" onPress={location.openSettings} />
    </View>
  );
}

On Android, openSettings already lands on the app-specific permissions page (the permission parameter is ignored), and Permissions.BUNDLES.LOCATION_BACKGROUND handles the second prompt via the ACCESS_BACKGROUND_LOCATION entry. The iOS branch above is the asymmetry that the bundle cannot hide.

A dedicated upgradeToAlways() helper that triggers the iOS system re-prompt in-app is tracked as future work (pending upstream react-native-permissions exposing the relevant API). For now, the Settings deep-link is the recommended pattern.

Handling partial grants (Android)

On Android, the user can grant foreground location and then deny background. Because each entry has its own blockedPrompt, the flow will surface the blocked modal for the background entry while keeping foreground granted. Use perms.blockedPermissions to show a summary row.

See also: