skip to content

In React Native, what does PermissionsAndroid's never_ask_again result mean, and how should a document-scanning app recover from it?

level: middleimportance: should knowfreq 44%

answer

  1. prompt is gone, request resolves instantly
  2. denied + no rationale = never_ask_again
  3. explain, then Linking.openSettings()
  4. re-check when the app is active again
  5. iOS: one prompt, then Settings only

basics

~10 s

never_ask_again means Android will no longer show the prompt, so request() resolves instantly; the scanner should explain why the camera is needed, offer Linking.openSettings(), and re-check the permission when the user returns.

solid answer

~40 s

React Native's native module resolves `'never_ask_again'` when the user refused and Android says a rationale should no longer be shown — the state where the system prompt is suppressed. From then on `PermissionsAndroid.request` resolves at once with no dialog, so calling it again does nothing. A document scanner should switch from asking to **explaining**: show an inline message saying the camera is needed to photograph documents and offer a button that calls `Linking.openSettings()`, which opens the app's page in the system Settings on both platforms. When the app becomes active again, call `check` and continue if the user enabled the camera. Meanwhile keep the app useful, for example by importing an existing image through the system photo picker. iOS behaves similarly after the first refusal, since its prompt appears only once.

code

tsx · 29 lines
tsx
import { useEffect } from 'react';
import { AppState, Linking, PermissionsAndroid, Pressable, Text, View } from 'react-native';

type Props = { onGranted: () => void; onImport: () => void };

export function CameraBlockedNotice({ onGranted, onImport }: Props) {
  useEffect(() => {
    const sub = AppState.addEventListener('change', async (state) => {
      if (state !== 'active') return;
      const granted = await PermissionsAndroid.check(
        PermissionsAndroid.PERMISSIONS.CAMERA,
      );
      if (granted) onGranted();
    });
    return () => sub.remove();
  }, [onGranted]);

  return (
    <View>
      <Text>Camera access is off, so the scanner cannot photograph documents.</Text>
      <Pressable accessibilityRole="button" onPress={() => Linking.openSettings()}>
        <Text>Open Settings</Text>
      </Pressable>
      <Pressable accessibilityRole="button" onPress={onImport}>
        <Text>Import an existing photo instead</Text>
      </Pressable>
    </View>
  );
}

go deeper

for a junior

Recall that never_ask_again means the system prompt is gone and that Linking.openSettings() sends the user to the app's Settings page.

for a middle

Explain how React Native derives the result from a refusal plus the rationale check, and why you re-check when the app becomes active again.

for a senior

Design one cross-platform permission state model with persisted last results, in-context explanations and a no-camera fallback, and rule out manifest bugs that mimic a refusal.

for a principal

Treat permanent refusal rates as a product metric, deciding which features may ask at all and what the app must still do for users who never grant access.

## What the result actually means `PermissionsAndroid.request` resolves one of three strings. React Native's Android module decides between the two negative ones after the system dialog closes: - if the permission was refused and Android's `shouldShowRequestPermissionRationale` returns **true**, the result is `'denied'` — the system will prompt again; - if it was refused and that call returns **false**, the result is `'never_ask_again'` — the system has stopped showing the prompt. In practice, on current Android versions, a user who refuses the same permission twice usually lands in the second state. Once there, every later `request` resolves `'never_ask_again'` immediately, without a dialog. One more cause: a permission missing from `AndroidManifest.xml` is refused without any prompt, and React Native reports that as `'never_ask_again'` too. Rule that out before designing a Settings flow. ## The iOS side of the same problem iOS shows each permission's system prompt **once**. After a refusal, the native module's request returns the denied status with no dialog, and only the Settings app can change it. There is no iOS equivalent of the rationale call; the whole recovery path is the Settings link. The `PermissionsAndroid` result names are Android-only, so shared code maps both platforms onto app-level states such as `granted`, `canAsk` and `blocked`. ## The recovery flow 1. **Stop prompting.** Another `request` call does nothing and teaches the user nothing. 2. **Explain in context.** On the scan screen, say what the camera is for — "to photograph receipts and contracts" — not that "permission is required". 3. **Offer a way out.** A button calling `Linking.openSettings()` opens the app's own page in the system Settings; the React Native docs describe it as opening the Settings app at the app's custom settings. 4. **Re-check on return.** Listen for the app becoming active again and call `PermissionsAndroid.check` (or the camera library's status call on iOS). If it is now `true`, open the camera without another tap. 5. **Degrade gracefully.** Keep a path that needs no camera, such as importing an existing photo or PDF through the system picker. ## Where the Settings link lands `Linking.openSettings()` returns a promise and leaves the app: - on **Android**, React Native starts the system's **app details** screen for your package, where the user opens **Permissions** and then **Camera**; - on **iOS**, it opens your app's page in the Settings app, which lists a toggle for each resource the app has asked about. Either way the user needs one or two more taps than they expect, so the in-app message should say exactly what to switch on: "Open Settings, tap Permissions, then allow Camera" on Android, "turn on Camera" on iOS. Copy that names the destination reduces the share of users who open Settings, look around and come back without changing anything. ## Handling the states in one place | App state | Android signal | iOS signal | What the scanner shows | |---|---|---|---| | `granted` | `check` is `true` | status granted | the camera preview | | `canAsk` | `check` is `false`, never asked or last result `'denied'` | not yet determined | a Scan button that triggers the prompt | | `blocked` | last result `'never_ask_again'` | denied | explanation, Settings button, import option | Because `check` returns only a boolean, the app cannot tell `canAsk` from `blocked` on Android without calling `request`. Many apps store the last result they received, per permission, so they can render the right screen before the user taps anything. ## Pitfalls - **Opening Settings unprompted.** Sending the user to Settings the moment the app starts feels hostile; the link belongs next to the feature. - **Treating `'denied'` as `'never_ask_again'`.** After a first refusal, asking again later, in context, is allowed and often succeeds. - **Forgetting the return trip.** Without a re-check, the user enables the camera, comes back, and still sees the blocked screen. - **Blaming the user for a manifest bug.** An undeclared permission also produces `'never_ask_again'`. ## What interviewers look for A strong answer names the mechanism (denied plus no rationale), shows the two-step recovery — explain, then `Linking.openSettings()` — remembers to re-check when the app resumes, and notes that iOS reaches the same state after a single refusal.

  • Why does calling PermissionsAndroid.request again after never_ask_again not help, even with a rationale object?
    The rationale dialog is shown only when Android says a rationale is appropriate, which is false in this state, and the system prompt itself is suppressed. So `request` skips both and resolves `'never_ask_again'` at once. Only a change in the Settings app can grant the permission now.
  • How can a React Native Android app show the right screen before the user taps Scan, given check() only returns a boolean?
    Persist the last status `request` returned for each permission, for example in local storage. On launch, `check` true means granted; false plus a stored `'never_ask_again'` means blocked; anything else means the app may still ask. Clear the stored status when `check` later reports true.

saying these in an interview costs you the question

  • Calling request() again will eventually bring the prompt back.
  • never_ask_again only happens when the user ticks a checkbox.
  • Linking.openSettings() opens the permission dialog inside the app.
  • iOS lets the app show its camera prompt again after a denial.
  • denied and never_ask_again need the same handling.