BuildBot

React, Off the Web

Native capabilities and permissions

Lesson 3 of 5

What you'll learn

  • Understand the two-step permissions model: declare at build time, request at runtime
  • Use expo-camera and expo-notifications through their permission hooks
  • Know what config plugins in app.json actually do to the native project

On the web, navigator.mediaDevices.getUserMedia just prompts the user. On mobile, the OS is stricter: sensitive capabilities are declared at build time (with the human-readable reason iOS shows in the prompt) and requested at runtime. Skip the declaration and the runtime request doesn't prompt — on iOS the app can crash outright. Both halves live in your code and app.json; Expo's libraries wrap each capability with a matching permission hook.

import { CameraView, useCameraPermissions } from "expo-camera";
import { Button, Text, View } from "react-native";

export default function Scanner() {
  const [permission, requestPermission] = useCameraPermissions();

  if (!permission) return null; // still loading
  if (!permission.granted) {
    return (
      <View>
        <Text>We need camera access to scan codes.</Text>
        <Button title="Allow camera" onPress={requestPermission} />
      </View>
    );
  }
  return <CameraView style={{ flex: 1 }} facing="back" />;
}

Permission status is a tiny state machine: undetermined (never asked — asking shows the system prompt), granted, or denied. On iOS, denied is sticky: the OS won't prompt again, so your recovery path is Linking.openSettings() plus a good explanation. Notifications follow the same shape, with delivery on top:

import * as Notifications from "expo-notifications";

const { status } = await Notifications.requestPermissionsAsync();
if (status === "granted") {
  await Notifications.scheduleNotificationAsync({
    content: { title: "Streak saved", body: "Lesson 3 complete." },
    trigger: { seconds: 60 * 60 * 24 },
  });
}

Local notifications like this one are scheduled on-device. Push notifications add a server: you obtain an Expo push token for the device and POST to Expo's push API, which relays through APNs and FCM.

Config plugins

Where do build-time declarations live? In app.json, under plugins. A config plugin is a function that rewrites the native project (Info.plist, AndroidManifest.xml, gradle files) during npx expo prebuild — native configuration expressed as JSON, no Xcode required:

// app.json (excerpt)
{
  "expo": {
    "plugins": [
      ["expo-camera", { "cameraPermission": "Allow $(PRODUCT_NAME) to scan QR codes." }],
      ["expo-notifications", { "icon": "./assets/notification-icon.png" }]
    ]
  }
}

Expo Go won't show you everything

Expo Go ships a fixed set of native modules, so config plugins and push notifications don't take effect there. Once you touch this layer, test in a development build (lesson 5's eas build) — Expo Go becomes a quick-preview tool only.

The challenge models the permission state machine — including sticky denial — around a fake camera.

The permission gate (JS model)

Run it, then try both buttons. Note that once denied, asking again does nothing — like iOS, the only way back is Settings.

Loading editor…
Knowledge check

Your camera permission request never prompts the user on iOS. What's the most likely cause?

Next: keeping users signed in — secure storage, session tokens, and why OAuth on native goes through the browser.

Saved on this device. Sign in to sync your progress everywhere.