← Back to the library
MobileCore · 7 min

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

  1. 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.
  2. 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.
  3. 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.
  4. 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.
  5. 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

SOURCES

Guide reviewed