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.
Run it, then try both buttons. Note that once denied, asking again does nothing — like iOS, the only way back is Settings.
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.