◉CLAUDE LABJP
●2.1.301 — Adds the vim % motion, LaTeX math rendering in the VS Code panel, and a retry-watchdog limit for usage-limit waits (10/15)●2.1.299 — Conversations over 200K tokens left idle about 54 minutes get compacted; MCP allow rules now match only the server they name●SONNET 4.5 — Retires on the Claude API on 11/30, 45 days left. Migrate to Sonnet 5.5●WINDOWS — Reports say hooks break silently only on Japanese-locale Windows; check the character encoding first●FAST MODE — Opus 5.5 fast mode is out of research preview and no longer needs the beta header (10/15)●NEW — I Put a Ten-Line Promise File at the To…●2.1.301 — Adds the vim % motion, LaTeX math rendering in the VS Code panel, and a retry-watchdog limit for usage-limit waits (10/15)●2.1.299 — Conversations over 200K tokens left idle about 54 minutes get compacted; MCP allow rules now match only the server they name●SONNET 4.5 — Retires on the Claude API on 11/30, 45 days left. Migrate to Sonnet 5.5●WINDOWS — Reports say hooks break silently only on Japanese-locale Windows; check the character encoding first●FAST MODE — Opus 5.5 fast mode is out of research preview and no longer needs the beta header (10/15)●NEW — I Put a Ten-Line Promise File at the To…
Articles/Claude Code
⟐ Claude Code/2026-05-06Intermediate

react-native-permissions vs Expo Permissions API — Claude Code

Should you use react-native-permissions or the Expo permissions API? This guide clarifies the decision criteria and shows practical iOS/Android implementation patterns using Claude Code, including common pitfalls and Privacy Manifest requirements.

Claude Code264React Native3Exporeact-native-permissionsmobile development3iOS26Android8permissions10

"Should I use react-native-permissions or a different library?" — this question shows up in nearly every React Native project that needs access to device hardware like camera, location, or microphone. It sounds deceptively simple, but the right answer depends entirely on how your project is structured. Getting this decision wrong early creates a cascade of configuration issues that tend to surface at the worst possible time: right before an App Store or Google Play submission.

This guide walks through the decision framework, provides working implementation examples for both approaches, and shows how Claude Code can accelerate the entire setup process — from the initial library choice through to the edge cases that catch most developers off guard.

Your Workflow Determines the Answer

There's no universally "better" library. The correct choice follows directly from your project's workflow type.

If you're using Expo Managed Workflow, use Expo's individual permission packages: expo-location, expo-camera, expo-media-library, expo-contacts, and so on. The older expo-permissions package was deprecated several years ago, but it still appears in older blog posts and Stack Overflow answers, which is why developers sometimes start with it and run into problems.

Each Expo SDK package now bundles its own permissions handling. The configuration integrates directly with app.json, so you don't need to manually edit Info.plist or AndroidManifest.xml. When the managed build system runs, it generates the native configuration files from your app.json automatically.

If you're using Expo Bare Workflow or React Native CLI, react-native-permissions is the current community standard. It provides a unified API that abstracts the differences between iOS and Android, letting you write one set of permission logic rather than maintaining separate implementations for each platform.

When you describe your project structure to Claude Code, it makes this determination for you and generates the appropriate implementation — the right library, with the right configuration files, for your setup.

Core Implementation with react-native-permissions

For a bare workflow project, here's the kind of implementation Claude Code produces when you ask it to set up camera and location permissions:

import { Platform } from 'react-native';
import {
  PERMISSIONS,
  RESULTS,
  check,
  request,
  openSettings,
} from 'react-native-permissions';
 
// Platform-specific permission constants
const CAMERA_PERMISSION =
  Platform.OS === 'ios'
    ? PERMISSIONS.IOS.CAMERA
    : PERMISSIONS.ANDROID.CAMERA;
 
const LOCATION_PERMISSION =
  Platform.OS === 'ios'
    ? PERMISSIONS.IOS.LOCATION_WHEN_IN_USE
    : PERMISSIONS.ANDROID.ACCESS_FINE_LOCATION;
 
/**
 * Request camera permission.
 * If permanently denied (BLOCKED), redirects to system Settings and returns false.
 */
export async function requestCameraPermission(): Promise<boolean> {
  // Always check current status before triggering a dialog
  const currentStatus = await check(CAMERA_PERMISSION);
 
  if (currentStatus === RESULTS.GRANTED) {
    return true;
  }
 
  if (currentStatus === RESULTS.BLOCKED) {
    // BLOCKED means the user has permanently denied the permission.
    // The system dialog will NOT appear again — redirect to Settings instead.
    await openSettings();
    return false;
  }
 
  // Status is DENIED (first-time request) — show the system dialog
  const result = await request(CAMERA_PERMISSION);
  return result === RESULTS.GRANTED;
}

The most important design decision here is separating check() from request(). On iOS, once a user has permanently denied a permission, the RESULTS.BLOCKED state is set and calling request() does absolutely nothing — the system dialog won't appear. This is one of the most frequently misunderstood behaviors in React Native permissions handling.

Code that skips the check() and calls request() directly appears to work correctly on the first run. Then, after the user denies the permission once, every subsequent call silently fails with no feedback. That kind of intermittent, user-state-dependent bug is genuinely difficult to reproduce and debug.

The corresponding Info.plist and AndroidManifest.xml entries that Claude Code also generates look like this:

<!-- Info.plist (iOS) -->
<key>NSCameraUsageDescription</key>
<string>Camera access is required to take photos.</string>
<key>NSLocationWhenInUseUsageDescription</key>
<string>Location access is used to show nearby results.</string>
<!-- AndroidManifest.xml -->
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />

Expo Managed Workflow: The app.json Approach

For Expo managed workflow projects, the same location permission using expo-location looks like this:

import * as Location from 'expo-location';
 
export async function requestLocationPermission(): Promise<boolean> {
  // requestForegroundPermissionsAsync handles the check + request in one call
  const { status } = await Location.requestForegroundPermissionsAsync();
 
  if (status !== 'granted') {
    // You can check status === 'denied' vs 'undetermined' for more granular handling
    console.warn('Location permission not granted. Status:', status);
    return false;
  }
 
  return true;
}

And the app.json configuration that drives the native setup:

{
  "expo": {
    "ios": {
      "infoPlist": {
        "NSLocationWhenInUseUsageDescription": "Used to display your current location on the map."
      }
    },
    "android": {
      "permissions": ["ACCESS_FINE_LOCATION"]
    }
  }
}

A key advantage of the Expo approach is that the app.json acts as the single source of truth. When you tell Claude Code to "update app.json as well," it generates both the TypeScript implementation and the configuration changes in one pass, keeping them consistent. Missing the app.json update is one of the most common reasons permission-related changes work locally but fail in a production build.

Building a Custom Hook with Permission State Caching

Both approaches benefit from centralizing permission state in a custom hook rather than checking permissions on every component mount. Here's a pattern Claude Code can generate when you ask for a reusable hook:

import { useEffect, useState } from 'react';
import { PERMISSIONS, RESULTS, check, request, openSettings } from 'react-native-permissions';
import { Platform } from 'react-native';
 
type PermissionStatus = 'undetermined' | 'granted' | 'denied' | 'blocked';
 
export function useCameraPermission() {
  const [status, setStatus] = useState<PermissionStatus>('undetermined');
  const [isChecking, setIsChecking] = useState(true);
 
  const permission =
    Platform.OS === 'ios' ? PERMISSIONS.IOS.CAMERA : PERMISSIONS.ANDROID.CAMERA;
 
  useEffect(() => {
    // Check once on mount — don't re-check on every render
    check(permission)
      .then((result) => {
        if (result === RESULTS.GRANTED) setStatus('granted');
        else if (result === RESULTS.BLOCKED) setStatus('blocked');
        else if (result === RESULTS.DENIED) setStatus('denied');
        else setStatus('undetermined');
      })
      .finally(() => setIsChecking(false));
  }, []);
 
  const requestPermission = async () => {
    if (status === 'blocked') {
      await openSettings();
      return false;
    }
    const result = await request(permission);
    const granted = result === RESULTS.GRANTED;
    setStatus(granted ? 'granted' : 'denied');
    return granted;
  };
 
  return { status, isChecking, requestPermission };
}

This pattern avoids the common mistake of triggering repeated permission requests on component re-renders. The permission is checked once on mount and cached in component state. From there, requestPermission handles the BLOCKED redirect and updates the cached status after the user responds.

Three Common Pitfalls

Pitfall 1: UNAVAILABLE on the iOS Simulator

Some permissions — camera, Bluetooth, motion sensors, NFC — return UNAVAILABLE in the iOS simulator because the underlying hardware doesn't exist. Many developers encounter this early in development and assume their implementation is broken when it's actually behaving correctly.

Asking Claude Code "which permissions return UNAVAILABLE on the iOS simulator and how should my code handle each case?" gives you a quick reference map of what needs real device testing versus what can be covered with mocks or conditional skips.

Pitfall 2: The Privacy Manifest Requirement (iOS)

Since 2024, Apple requires apps that access certain privacy-sensitive APIs to include a PrivacyInfo.xcprivacy file describing how those APIs are used. If you're using react-native-permissions with camera, location, contacts, or other sensitive hardware, and your PrivacyInfo.xcprivacy file is missing or incomplete, your App Store submission will be rejected.

Claude Code can generate the correct PrivacyInfo.xcprivacy content when you describe which permissions your app uses. This is one of those requirements that isn't well-documented in the main React Native permissions guides but shows up reliably as a rejection reason.

Pitfall 3: Android Permission Groups

On Android, some permissions belong to the same permission group (for example, READ_CONTACTS and WRITE_CONTACTS are both in the CONTACTS group). When the user grants one permission in a group, Android may automatically grant others in the same group. This can cause your permission checks to return GRANTED for permissions you never explicitly requested, which can mask bugs in your implementation.

Claude Code handles this correctly when generating permission logic, but if you're reviewing existing code, it's worth verifying that the code isn't relying on implicit group grants.

How to Prompt Claude Code Effectively

The most reliable approach is leading with your project constraints before the implementation request:

This is an Expo bare workflow project.
Minimum targets: iOS 16.0 and Android API 33.
Library: react-native-permissions v4.

Implement a custom hook (usePermissions) that manages camera, microphone,
and push notification permissions.

For each permission, handle GRANTED / BLOCKED / DENIED states.
When BLOCKED, show an alert explaining why the permission is needed,
with a button that opens the system Settings app.
Also generate the required Info.plist entries and AndroidManifest.xml entries.

Providing the library version prevents Claude Code from generating code that uses a deprecated API. Providing minimum OS targets helps it apply the correct conditional behavior, since some permission APIs changed behavior between iOS versions.


Permissions management sits at the intersection of user experience, app functionality, and store submission compliance. The BLOCKED state handling, simulator behavior, and Privacy Manifest requirement are the three areas where most implementations fall short — not because they're complicated, but because they're easy to skip when you're focused on getting the core feature working.

Getting the foundation right here saves a significant amount of reactive debugging in the weeks before launch.

For a broader look at integrating Claude Code into your Expo/React Native workflow, see Claude Code × Expo/React Native — Accelerating Cross-Platform App Development.

Start with a Claude Code review of your existing permissions code — ask it to check whether your BLOCKED state handling is correct and whether your Privacy Manifest is complete. That's where the most common issues hide.

Share

Thank You for Reading

Claude 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

⟐ Claude Code2026-09-04
The One File I Keep Out of Claude Code's Reach: project.pbxproj
With Xcode project files, the breakage that still opens costs far more than the breakage that refuses to open. Here is what I measured before moving every edit behind a script, and where the line sits today.
⟐ Claude Code2026-10-12
After Moving to SPM, My App Died the Moment It Launched — Reading One dyld Line with Claude Code
Right after moving from CocoaPods to Swift Package Manager, the build passed but the app died on launch. Here is how I traced one dyld line to its cause, the fix, and how to verify it.
⟐ Claude Code2026-08-27
Trusting the allow rules in your repo, or moving them to the environment
On disposable machines, the permissions.allow rules committed to your repo are dropped while the workspace waits to be trusted. Here is what gets dropped and what survives on 2.1.246, where each kind of rule belongs, and a preflight that catches the gap before a run starts.