A feature stays broken after the user says no to a permission
“Someone denied camera or location once, and now tapping the button does nothing: no prompt, no message. Some users on iOS chose Allow Once and lose location the next day.”
THE PRINCIPLE
A permission is state you re-read, not a flag you got once. Every feature that needs one needs a designed denied state.
FIRST MOVES
- Check before each use and on app resume, not just at first launch; users change permissions in Settings, Android auto-resets permissions of unused apps, and one-time grants expire.
- Model 'can I ask again?': on Android 11+ denying twice is permanent (shouldShowRequestPermissionRationale is false and the dialog no longer appears); on iOS you get one prompt per permission. In Expo, read canAskAgain from the PermissionResponse.
- When blocked, replace the dead button with an inline explanation and an Open Settings action (Linking.openSettings()). Don't nag or block the whole app.
- Handle partial grants: iOS Allow Once is temporary When In Use that reverts to not determined; Core Location ignores later Always requests while that temporary grant lasts. Android also offers Only this time, approximate location and limited photo access.
- Ask in context, after a user action that explains why, so the scarce prompt isn't spent on launch. Test with adb shell pm clear-permission-flags and by reinstalling on iOS.
TOOLS: THEN → NOW
Asking for every permission on first launchContextual requests with a pre-prompt explaining the benefit
Boolean granted checksStatus plus canAskAgain from expo-* modules or react-native-permissions (blocked vs denied), with Linking.openSettings()
PATTERN SNAPSHOT
import * as Location from "expo-location";
async function ensureLocation(): Promise<"ready" | "explain" | "settings"> {
const current = await Location.getForegroundPermissionsAsync();
if (current.granted) return "ready";
if (!current.canAskAgain) return "settings"; // the OS won't show a prompt
const next = await Location.requestForegroundPermissionsAsync();
if (next.granted) return "ready";
return next.canAskAgain ? "explain" : "settings";
}
// UI: "settings" renders an inline message with a button calling Linking.openSettings().CLOSE THE AI. EXPLAIN THIS.
On Android 11+, how does the app tell "never asked" from "denied twice", given that both return denied before requesting?HOW IT WORKS UNDERNEATH
RELATED SYMPTOMS
SOURCES
- Android Developers: Request runtime permissions (opens in new tab)Checked
- Apple: requestWhenInUseAuthorization() (opens in new tab)Checked
- Apple: requestAlwaysAuthorization() (opens in new tab)Checked
- Expo Location: permissions and PermissionResponse (opens in new tab)Checked
- React Native: Linking.openSettings (opens in new tab)Checked
Guide reviewed