How Permissions Work
The operating system guards the camera, location and the photo library; your code can only ask, and a refusal usually sticks.
const [permission, requestPermission] = useCameraPermissions();A permission is the operating system's gate in front of private data and hardware: the camera, the microphone, the photo library, location, notifications. JavaScript cannot open that gate. It can only ask, and the person holding the phone answers. Nothing in a browser prepares you for this. There is no navigator.permissions here, and no permission is ever granted by writing code.
Each library owns its own
There is no central permissions module in React Native 0.86 with Expo SDK 57. The camera's permission lives in expo-camera, location's in expo-location, and so on. The old expo-permissions package was removed years ago; code that imports it is out of date.
The three states
Every check or request resolves to a permission response object with status (one of undetermined, granted, denied), a granted boolean, a canAskAgain boolean and expires. Reading permission.granted is shorthand for comparing permission.status to the granted value: the same information, one field instead of two.
Ask at the moment of use
Request when the user taps the thing that needs it, not at launch. A launch-time wall asks for access to a feature the user has not seen yet, and it gets refused.
A refusal is close to permanent
iOS shows its dialog once per permission. After a denial, later requests resolve immediately with the stored answer and no dialog appears; canAskAgain is then false. Android allows another attempt, but treats repeated refusals as final. Once that happens, only the Settings app can change it.
Example
import { useCameraPermissions } from 'expo-camera';
import { Button, StyleSheet, Text, View } from 'react-native';
export default function CameraGate() {
// The hook holds the last known answer and gives you a function to ask.
const [permission, requestPermission] = useCameraPermissions();
// null means the hook has not read the stored status yet.
if (permission === null) {
return (
<View style={styles.screen}>
<Text style={styles.body}>Checking camera access</Text>
</View>
);
}
if (!permission.granted) {
return (
<View style={styles.screen}>
<Text style={styles.body}>
{permission.canAskAgain
? 'The scanner needs the camera to read a receipt.'
: 'Camera access is off. Turn it on in Settings.'}
</Text>
{/* Only offer the button while the system dialog can still appear. */}
{permission.canAskAgain ? (
<Button title="Allow camera" onPress={() => { void requestPermission(); }} />
) : null}
</View>
);
}
return (
<View style={styles.screen}>
<Text style={styles.body}>Camera ready</Text>
</View>
);
}
const styles = StyleSheet.create({
// flexDirection defaults to column here, not row as in CSS.
screen: { flex: 1, justifyContent: 'center', padding: 24 },
body: { fontSize: 16, marginBottom: 12 },
});When to use it
- Deciding whether a camera screen should show a preview, an explanation or a link to Settings.
- Hiding a Nearby tab when location has been refused rather than letting it fail on open.
- Logging how many users reach the system dialog and how many grant it.
More examples
The decision as a pure function
Four states, one function, no component involved. Reach for this when the same permission gate appears on several screens: the branching logic gets unit tests, and each screen only maps the returned string to markup.
// A permission response only ever leads to one of four screens.
// Keeping that choice in a plain function makes it testable.
type PermissionLike = { granted: boolean; canAskAgain: boolean } | null;
export function nextStep(
permission: PermissionLike
): 'loading' | 'ask' | 'settings' | 'ready' {
if (permission === null) return 'loading';
if (permission.granted) return 'ready';
return permission.canAskAgain ? 'ask' : 'settings';
}
console.log(nextStep(null));
console.log(nextStep({ granted: false, canAskAgain: true }));
console.log(nextStep({ granted: false, canAskAgain: false }));
console.log(nextStep({ granted: true, canAskAgain: false }));
// Output:
// loading
// ask
// settings
// readyRequesting inside an event handler
The imperative form: `requestForegroundPermissionsAsync` returns a promise instead of driving a hook. Use it when the request belongs to an action rather than to a screen's render, such as a button that reads the position once. Tapping the button shows the system dialog the first time and the coordinates or an explanation afterwards.
import { useState } from 'react';
import * as Location from 'expo-location';
import { Button, StyleSheet, Text, View } from 'react-native';
export default function CoordsScreen() {
const [label, setLabel] = useState('No location yet');
async function showCoords() {
// The promise resolves with the same shape the hooks expose.
const response = await Location.requestForegroundPermissionsAsync();
if (!response.granted) {
setLabel(
response.canAskAgain
? 'Location is needed to tag this note.'
: 'Location is off for this app. Change it in Settings.'
);
return;
}
const position = await Location.getCurrentPositionAsync({});
setLabel(
`${position.coords.latitude.toFixed(3)}, ${position.coords.longitude.toFixed(3)}`
);
}
return (
<View style={styles.screen}>
<Text style={styles.body}>{label}</Text>
<Button title="Tag with my location" onPress={() => { void showCoords(); }} />
</View>
);
}
const styles = StyleSheet.create({
screen: { flex: 1, justifyContent: 'center', padding: 24 },
body: { fontSize: 16, marginBottom: 12 },
});Checking without asking
Every Expo module pairs a `get...PermissionsAsync` with a `request...PermissionsAsync`. The get form is silent, so it is safe to call while a screen mounts; the request form may show the one dialog iOS allows and should wait for a deliberate tap.
import * as Location from 'expo-location';
// Reading the status never shows a dialog. Only the request functions do.
// Use this to decide what the screen looks like before the user taps anything.
export async function locationIsReady(): Promise<boolean> {
const response = await Location.getForegroundPermissionsAsync();
return response.granted;
}
Discussion