Mobile Apps with React Native and Expo · Device Features, Performance, Testing and Shipping

Camera, Location and Permissions Done Right

Use device features through Expo modules, and handle every permission state: not asked yet, granted, denied, and denied forever. Explain why before asking, and degrade gracefully when the answer is no.

Create a free account to save this page, take notes and track your progress. Sign in

What you'll build

A "report a problem" screen: the user takes a photo and the app attaches their current location. If they refuse camera or location access, the screen still works (pick a photo from the library, type an address) and explains how to enable access in Settings.

The idea

Every sensitive feature (camera, photos, location, contacts, microphone) needs user permission. Each permission can be in several states:

State Meaning What to do
undetermined Never asked Explain the benefit, then ask
granted Allowed Use the feature
denied, can ask again User said no once (Android) Explain again, ask again only if they act
denied, can't ask again User said "don't ask" (iOS after the first no; Android after repeated no) Offer a button that opens the app's Settings

Rules of thumb:

  • Ask only when the user starts the feature, with a short explanation first.
  • Request the least access: "while using the app" location, not "always"; a selected photo, not the whole library.
  • iOS requires a usage description string for each permission in app.json (Expo config plugins add these); missing ones get the app rejected or crash.
  • Always offer a fallback path.

The code

import { useState } from "react";
import { Button, Linking, Text, View } from "react-native";
import { CameraView, useCameraPermissions } from "expo-camera";
import * as Location from "expo-location";

export function ReportProblem() {
  const [permission, requestPermission] = useCameraPermissions();
  const [coords, setCoords] = useState<string | null>(null);

  if (!permission) return null;                                  // still loading
  if (!permission.granted) {
    return (
      <View style={{ padding: 16, gap: 12 }}>
        <Text>We use the camera to photograph the problem you're reporting.</Text>
        {permission.canAskAgain
          ? <Button title="Allow camera" onPress={requestPermission} />
          : <Button title="Open Settings" onPress={() => Linking.openSettings()} />}
      </View>
    );
  }

  async function attachLocation() {
    const { status } = await Location.requestForegroundPermissionsAsync();   // "while using the app"
    if (status !== "granted") return setCoords("Location not shared: type the address instead");
    const pos = await Location.getCurrentPositionAsync({ accuracy: Location.Accuracy.Balanced });
    setCoords(`${pos.coords.latitude.toFixed(5)}, ${pos.coords.longitude.toFixed(5)}`);
  }

  return (
    <View style={{ flex: 1 }}>
      <CameraView style={{ flex: 1 }} facing="back" />
      <Button title="Add my location" onPress={attachLocation} />
      {coords && <Text style={{ padding: 16 }}>{coords}</Text>}
    </View>
  );
}
// app.json (excerpt): explain why, in the user's language
{
  "expo": {
    "plugins": [
      ["expo-camera", { "cameraPermission": "Take a photo of the problem you're reporting." }],
      ["expo-location", { "locationWhenInUsePermission": "Attach where the problem is to your report." }]
    ]
  }
}

Try the logic

Permission handling is a small state machine. Modelling it in plain TypeScript makes sure every state leads somewhere sensible.

What to show for each permission stateTry it yourself
type Permission = { status: "undetermined" | "granted" | "denied"; canAskAgain: boolean };
type Screen = "use the camera" | "explain, then ask" | "explain, offer 'Open Settings'" | "explain, ask again";

function nextStep(p: Permission): Screen {
  if (p.status === "granted") return "use the camera";
  if (p.status === "undetermined") return "explain, then ask";
  return p.canAskAgain ? "explain, ask again" : "explain, offer 'Open Settings'";
}

// A user's journey: first visit, says no, comes back, says no again with "don't ask", later enables in Settings.
const journey: [string, Permission][] = [
  ["first visit", { status: "undetermined", canAskAgain: true }],
  ["denied once (Android)", { status: "denied", canAskAgain: true }],
  ["denied for good", { status: "denied", canAskAgain: false }],
  ["enabled in Settings", { status: "granted", canAskAgain: true }],
];
for (const [moment, permission] of journey) {
  console.log(moment.padEnd(24), "->", nextStep(permission));
}

// Least access: pick location accuracy by purpose.
const accuracyFor = (purpose: "city weather" | "report address" | "turn-by-turn") =>
  ({ "city weather": "lowest (~3 km)", "report address": "balanced (~100 m)", "turn-by-turn": "best for navigation" })[purpose];
console.log("report address needs:", accuracyFor("report address"));

Output

first visit              -> explain, then ask
denied once (Android)    -> explain, ask again
denied for good          -> explain, offer 'Open Settings'
enabled in Settings      -> use the camera
report address needs: balanced (~100 m)

Common mistakes

  • Asking at launch for everything. Users deny what they don't understand.
  • Dead ends after "no". Always show a fallback or a Settings button.
  • Missing iOS usage strings, which cause a crash on request or a store rejection.
  • Asking for "always" location when "while using" is enough; stores ask for justification.
  • Not re-checking on return from Settings. Check permissions again when the app becomes active.

Key points

  • Ask in context, with a reason, for the least access needed.
  • Handle undetermined, granted, denied-can-ask and denied-forever.
  • Provide fallbacks and an "Open Settings" path.
  • Configure usage descriptions through Expo config plugins.

Further reading: Permissions in Expo, expo-camera, expo-location.

Practice questions

What should an app do when a user has permanently denied camera permission?

Answer

It can't show the system prompt again, so it should explain why the feature needs the camera, offer a fallback (e.g. pick from the library), and provide a button that opens the app's Settings page (Linking.openSettings()). Re-check the permission when the app returns to the foreground.

What does 'least access' mean for location permission?

Answer

Request only what the feature needs: foreground ('while using the app') rather than background access, and coarse or balanced accuracy unless precise location is essential.

Open in the app to mark lessons complete, take notes and track your progress.

Was this page helpful?