EthicsComputerCourses
Join training +91 90532 10052

Camera, Location & Permissions

React Native · Lesson 19 of 30 Intermediate 7 min read Updated

By the end of this lesson you will be able to ask the user for permission the right way, take a photo with the camera, scan a QR code, and find the user's location. You will also handle the user who says "No", which is where many apps go wrong.

Device features are what make a phone app more useful than a website: a delivery app that knows where you are, a shop app that scans a QR code to pay. Expo provides ready-made libraries for each feature, and the ones in this lesson work in Expo Go.

How permissions work

Phones protect private things (camera, microphone, location, photos, contacts) behind runtime permissions. The app must ask while it is running, and the user can say yes or no. It is like a guard at a housing society gate: the visitor must ask, and the resident decides.

A permission has three states you care about:

StatusMeaningWhat your app should do
undeterminedNever askedExplain why, then ask
grantedUser said yesUse the feature
deniedUser said noExplain the benefit; if you cannot ask again, offer to open Settings

Every Expo permission response also has canAskAgain. After the user denies (sometimes twice on Android), the system stops showing the dialog, and only the phone's Settings app can change it.

Good habits: ask only when the user taps something that needs the feature, never on the first launch for everything. And always have a plan for "denied".

Practise the logic: what to show

The decision about which screen to show is plain JavaScript, so you can try it here. Each object has the same shape as an Expo permission response.

React JSXTry it yourself
import { createRoot } from "react-dom/client";

function permissionView(permission) {
  if (!permission) return "loading";
  if (permission.granted) return "camera";
  if (permission.status === "undetermined" || permission.canAskAgain) return "explain-and-ask";
  return "open-settings";
}

const cases = [
  null,
  { status: "undetermined", granted: false, canAskAgain: true },
  { status: "granted", granted: true, canAskAgain: true },
  { status: "denied", granted: false, canAskAgain: true },
  { status: "denied", granted: false, canAskAgain: false },
];

cases.forEach((c) => console.log(JSON.stringify(c), "->", permissionView(c)));

createRoot(document.getElementById("root")).render(
  <ul>{cases.map((c, i) => <li key={i}>{c ? c.status : "null"}: {permissionView(c)}</li>)}</ul>
);
Output
null -> loading
{"status":"undetermined","granted":false,"canAskAgain":true} -> explain-and-ask
{"status":"granted","granted":true,"canAskAgain":true} -> camera
{"status":"denied","granted":false,"canAskAgain":true} -> explain-and-ask
{"status":"denied","granted":false,"canAskAgain":false} -> open-settings

You will use exactly this function in the camera screen below.

Step 1: describe why you need each permission

iOS shows your own sentence in the permission dialog, and app stores reject vague reasons. Set the messages in app.json through each library's config plugin (a plugin that edits the native project during a build):

JSON
{
  "expo": {
    "plugins": [
      ["expo-camera", { "cameraPermission": "Allow Chai App to use the camera to scan payment QR codes." }],
      ["expo-location", { "locationWhenInUsePermission": "Allow Chai App to find tea stalls near you." }]
    ]
  }
}

Expo Go has its own fixed messages, so you see your text only in a development or release build.

Step 2: a camera screen that scans QR codes

Terminal
npx expo install expo-camera
React JSX
import { useRef, useState } from "react";
import { View, Text, Pressable, Linking } from "react-native";
import { CameraView, useCameraPermissions } from "expo-camera";

export default function ScanScreen() {
  const [permission, requestPermission] = useCameraPermissions();
  const [scanned, setScanned] = useState(null);
  const cameraRef = useRef(null);

  if (!permission) return <View />; // still checking
  if (!permission.granted) {
    return (
      <View style={{ flex: 1, justifyContent: "center", padding: 24, gap: 12 }}>
        <Text>We need the camera to scan payment QR codes.</Text>
        {permission.canAskAgain ? (
          <Pressable onPress={requestPermission}><Text style={{ color: "#087ea4" }}>Allow camera</Text></Pressable>
        ) : (
          <Pressable onPress={() => Linking.openSettings()}><Text style={{ color: "#087ea4" }}>Open Settings</Text></Pressable>
        )}
      </View>
    );
  }

  async function takePhoto() {
    const photo = await cameraRef.current?.takePictureAsync();
    console.log("Saved photo at", photo?.uri);
  }

  return (
    <View style={{ flex: 1 }}>
      <CameraView
        ref={cameraRef}
        style={{ flex: 1 }}
        facing="back"
        barcodeScannerSettings={{ barcodeTypes: ["qr"] }}
        onBarcodeScanned={scanned ? undefined : ({ data }) => setScanned(data)}
      />
      <View style={{ padding: 16, gap: 8 }}>
        <Text>{scanned ? `Scanned: ${scanned}` : "Point the camera at a QR code"}</Text>
        <Pressable onPress={takePhoto}><Text>Take photo</Text></Pressable>
        {scanned ? <Pressable onPress={() => setScanned(null)}><Text>Scan again</Text></Pressable> : null}
      </View>
    </View>
  );
}

What you should see: the first time, a message and an "Allow camera" button. Tapping it shows the system dialog. After you allow it, the live camera fills the top of the screen. Point it at any QR code and the text below shows its contents; scanning stops until you tap "Scan again". "Take photo" saves a picture to the app's cache and logs a file:// address you can pass to an Image.

useCameraPermissions() returns the current permission (or null while loading) and a function to request it. Setting onBarcodeScanned to undefined after a scan stops the same code firing many times a second.

Step 3: find the user's location

Terminal
npx expo install expo-location
React JSX
import { useState } from "react";
import { View, Text, Pressable } from "react-native";
import * as Location from "expo-location";

export default function NearMe() {
  const [message, setMessage] = useState("Tap to find tea stalls near you");

  async function locate() {
    const { status } = await Location.requestForegroundPermissionsAsync();
    if (status !== "granted") {
      setMessage("Location is off. You can still search by city name.");
      return;
    }
    const position = await Location.getCurrentPositionAsync({ accuracy: Location.Accuracy.Balanced });
    const { latitude, longitude } = position.coords;
    setMessage(`You are at ${latitude.toFixed(4)}, ${longitude.toFixed(4)}`);
  }

  return (
    <View style={{ padding: 24, gap: 12 }}>
      <Text>{message}</Text>
      <Pressable onPress={locate}><Text style={{ color: "#087ea4" }}>Use my location</Text></Pressable>
    </View>
  );
}

What you should see: tapping "Use my location" shows the location dialog. After you allow it, the text changes to your coordinates, for example "You are at 26.9124, 75.7873" in Jaipur. If you deny it, the app still works with a city search.

Foreground permission covers location while the app is open. Tracking in the background (like a delivery partner app) needs extra permissions, a development build and much stronger reasons for the app stores.

Step 4: use the coordinates

Coordinates become useful when you calculate something, such as the distance to each shop. This is plain JavaScript, so try it here:

React JSXTry it yourself
import { createRoot } from "react-dom/client";

function distanceKm(a, b) {
  const R = 6371; // Earth's radius in km
  const toRad = (deg) => (deg * Math.PI) / 180;
  const dLat = toRad(b.latitude - a.latitude);
  const dLon = toRad(b.longitude - a.longitude);
  const h = Math.sin(dLat / 2) ** 2 + Math.cos(toRad(a.latitude)) * Math.cos(toRad(b.latitude)) * Math.sin(dLon / 2) ** 2;
  return 2 * R * Math.asin(Math.sqrt(h));
}

const me = { latitude: 26.9124, longitude: 75.7873 };
const shops = [
  { name: "Hawa Mahal stall", latitude: 26.9239, longitude: 75.8267 },
  { name: "Amer Fort cafe", latitude: 26.9855, longitude: 75.8513 },
  { name: "Johari Bazaar sweets", latitude: 26.9196, longitude: 75.8235 },
];

const sorted = shops
  .map((s) => ({ ...s, km: distanceKm(me, s) }))
  .toSorted((a, b) => a.km - b.km);

sorted.forEach((s) => console.log(s.name, s.km.toFixed(1) + " km"));
createRoot(document.getElementById("root")).render(
  <ol>{sorted.map((s) => <li key={s.name}>{s.name}: {s.km.toFixed(1)} km</li>)}</ol>
);
Output
Johari Bazaar sweets 3.7 km
Hawa Mahal stall 4.1 km
Amer Fort cafe 10.3 km

This "straight line" distance uses the haversine formula. Road distance is longer; for that you would call a maps API from your server.

Common mistakes

  • Asking for every permission at launch. Users say no to requests they do not understand. Ask in context.
  • No plan for "denied". Offer an alternative (city search) and a way to open Settings.
  • Scanning the same QR code 30 times. Turn off onBarcodeScanned after the first scan.
  • Vague permission messages. Explain the exact benefit in app.json.
  • Asking for background location without a real need. It drains battery and app stores review it strictly.

Key points

  • Permissions are asked at runtime; handle undetermined, granted and denied.
  • When canAskAgain is false, send the user to Settings with Linking.openSettings().
  • Set clear permission messages through config plugins in app.json.
  • expo-camera gives CameraView, useCameraPermissions, photos and QR scanning.
  • expo-location gives requestForegroundPermissionsAsync and getCurrentPositionAsync.
  • Always confirm what a scanned code contains before acting on it.

Exercise

Build a "Mark attendance" screen for a coaching centre. When the student taps "Check in", ask for foreground location. If granted, get the position and calculate the distance to the centre at latitude 28.8955, longitude 76.6066 (Rohtak). Show "Checked in" if the student is within 0.2 km, otherwise "You are X km away". If permission is denied, show "Please allow location to check in".

Show solution
React JSX
import { useState } from "react";
import { View, Text, Pressable } from "react-native";
import * as Location from "expo-location";
import { distanceKm } from "@/utils/distance"; // the function from Step 4

const CENTRE = { latitude: 28.8955, longitude: 76.6066 };

export default function CheckIn() {
  const [message, setMessage] = useState("");

  async function checkIn() {
    const { status } = await Location.requestForegroundPermissionsAsync();
    if (status !== "granted") {
      setMessage("Please allow location to check in");
      return;
    }
    const { coords } = await Location.getCurrentPositionAsync({ accuracy: Location.Accuracy.High });
    const km = distanceKm(coords, CENTRE);
    setMessage(km <= 0.2 ? "Checked in" : `You are ${km.toFixed(1)} km away`);
  }

  return (
    <View style={{ padding: 24, gap: 12 }}>
      <Pressable onPress={checkIn}><Text style={{ fontSize: 18 }}>Check in</Text></Pressable>
      <Text>{message}</Text>
    </View>
  );
}

coords already has latitude and longitude fields, so it can be passed straight to distanceKm. High accuracy is used because 200 metres is a small distance.

Quick Quiz

1. When is the best time to ask for camera permission?

2. The camera permission has granted: false and canAskAgain: false. What should the app offer?

3. Where do you set the sentence shown in the iOS camera permission dialog?

4. Which function asks for location while the app is open?

Learn React Native with a mentor

Free lessons get you started. Our Mobile App Development training adds real projects and code reviews from working developers.

  • Project-based learning
  • Mentor code reviews
  • Free counselling call