Back to blog
Guides7 min read

React Native permissions that satisfy both stores

Where Android grants and iOS usage descriptions live in an Expo or bare React Native app, why declaring a permission is only half of asking for it, and how to write usage descriptions App Review will accept.

Two platforms ask differently about the same feature

A React Native app that takes photos needs one thing from the developer's point of view — the camera — and two entirely different things from the platforms'. Android wants a permission string in its manifest. iOS wants a sentence, shown to the user in the consent prompt, explaining why the app is asking.

Declaring one and not the other is the single most common way a mobile feature ships broken: it works on the device it was built on, and fails on the other store. The failure modes are asymmetric, too. A missing Android grant throws at runtime. A missing iOS usage description is a rejection in review, which costs days rather than minutes.

The free React Native permissions generator emits both halves for each feature you pick, along with the install line for the library behind it. (In the bare workflow, the runtime half usually goes through the react-native-permissions library — one check/request API across permissions, with its native handlers opted in via the Podfile.)

Where the declarations live

In an Expo managed project, both go in app.json, and prebuild writes them into the native projects:

{
  "expo": {
    "android": { "permissions": ["android.permission.CAMERA"] },
    "ios": {
      "infoPlist": {
        "NSCameraUsageDescription": "Scan a receipt to attach it to an expense."
      }
    },
    "plugins": ["expo-router", "expo-camera"]
  }
}

The plugins array matters as much as the two declaration blocks: many Expo modules ship a config plugin that performs native setup prebuild cannot infer, and a module installed without its plugin registered is a module that behaves differently in a development build than in the store build.

In a bare project you edit the native files directly — <uses-permission android:name="…" /> lines in android/app/src/main/AndroidManifest.xml, and key/string pairs in ios/<App>/Info.plist. The vocabulary is identical; only the file changes. That is why it is worth deciding the permission set independently of the workflow you are on: switching from managed to bare later moves the declarations without rewriting them.

Declaring is not asking

A declaration makes it possible to ask. Both platforms still prompt the user the first time the feature is used, and the prompt comes from the library that owns the feature — useCameraPermissions in expo-camera, requestForegroundPermissionsAsync in expo-location, requestPermissionsAsync in expo-notifications.

The two halves fail in opposite directions, and both are worth recognising on sight:

  • Declared but never requested: nothing prompts, and the API returns a denied status the app probably does not check.
  • Requested but never declared: the request itself fails, on iOS often by crashing the app outright rather than returning a refusal.

Handle the third state as well. A permission is not a boolean — it is granted, denied, or denied-permanently, and the last one cannot be re-prompted. The app has to route the user to system settings instead, which means the feature needs a designed state for "you said no", not just a happy path.

Some grants come in pairs

Several capabilities need more than one Android permission, and emitting half of a pair produces an app that fails on a device rather than in a build:

  • Calendar is a read grant and a write grant.
  • Bluetooth split on Android 12 into BLUETOOTH_SCAN and BLUETOOTH_CONNECT.
  • Location has a coarse and a fine grant, and background location is a third, separate one — which on modern Android can only be requested after foreground location has already been allowed. A single combined prompt is not available; the sequence is the API.

Media access has its own version of this. Android 13 replaced blanket external-storage access with per-type grants (READ_MEDIA_IMAGES, READ_MEDIA_VIDEO), and reading the library is a different grant from saving to it. Asking for the write grant when the feature only picks an existing photo is a permission the app never uses and a reviewer will notice.

Usage descriptions are product copy, not boilerplate

Every NS*UsageDescription string is shown verbatim to the user. App Review rejects a missing one outright, and rejects a vague one — "This app needs camera access" — often enough that it is worth treating the string as copy someone writes rather than a field someone fills.

A usable one names the feature and the benefit:

Scan a receipt with your camera to attach it to an expense.

not

Camera access is required for the app to function.

The same sentence is a useful design check. If the reason is hard to write in one line, the feature is asking for more than it needs — that is usually the moment to notice that a photo picker would do the job without a camera grant at all, or that a coarse location is enough for a weather lookup.

This is why every description generated by the tool above is a visible TODO. A generator that produced plausible-sounding sentences would be handing out the rejection.

The grants that attract extra scrutiny

  • Background location. Both stores ask you to justify it, and both have rejected apps whose stated reason did not require it. If the feature is "show nearby places while I am looking at the map", foreground is the correct grant.
  • App tracking transparency. On iOS, reading the advertising identifier requires an explicit prompt of its own, with its own usage description, before any tracking SDK initialises.
  • Contacts and calendar. These are personal data belonging to people who are not your user and never agreed to anything. Uploading an address book to find friends is a decision with legal weight, not just a permission.
  • Microphone. Frequently requested by a video feature that only needs it sometimes. Ask when the recording starts, not at launch.

Ask late, and only for what a feature needs

The pattern that keeps grant rates high is the same on both platforms: request at the moment the feature is used, after the user has done something that makes the request obviously necessary, and show your own explanation screen before the system prompt when the reason is not self-evident. The system prompt can only be shown once — spending it on a cold app launch is spending it badly.

Working out the permission set early also tends to change the plan. In the Nodlume workspace a Mobile project derives its declarations from what the app actually contains, so a screen that uses the camera is what puts the camera permission in app.json, and nothing gets declared because it was pasted from a tutorial. To work through the set on its own, the React Native permissions generator is free and needs no account — and the Tauri permissions generator does the same for desktop builds, while browser extensions have their own.

Target-specific scaffolding

Continue this learning path

Permissions, manifests, capabilities, agent projects and MCP servers for browser extensions, desktop, mobile, and backend agents.

Explore platform generators

Design your application

Design Web, Desktop, Mobile, Terminal, or Extension projects with a target-aware canvas and exporter.

Open projects

We'd like to use Google cookies to understand how Nodlume is used and to measure our advertising. Nothing loads until you choose, and declining does not affect anything in the app.