skip to content

In a React Native app, how do you handle a user who has denied notification permission, including sending them to the system Settings?

level: middleimportance: should knowfreq 45%

answer

  1. can the system still ask?
  2. canAskAgain / never_ask_again
  3. Linking.openSettings()
  4. re-check on return to active
  5. degrade, do not nag

basics

~20 s

Check whether the system can still ask (canAskAgain in expo-notifications, never_ask_again from PermissionsAndroid). If not, explain the benefit and open the app's settings with Linking.openSettings(), then re-check the permission when the app returns to the foreground.

solid answer

~40 s

First distinguish a **soft** denial from a **hard** one. With expo-notifications, `getPermissionsAsync()` returns `canAskAgain`; with core `PermissionsAndroid.request`, a `'never_ask_again'` result means the same. On iOS any denial is hard, because the alert is shown once. If the system can still ask, wait for a meaningful moment and ask again in context. If it cannot, a request draws nothing, so show a short in-app message explaining what reminders give the user and a button that calls `Linking.openSettings()`, which opens the app's own page in Settings (on Android, `Linking.sendIntent('android.settings.APP_NOTIFICATION_SETTINGS', …)` can go straight to its notification screen). When the app becomes `active` again, re-read the permission rather than trusting a cached value, and meanwhile keep the feature usable without notifications.

code

tsx · 27 lines
tsx
import * as Notifications from 'expo-notifications';
import { useEffect, useState } from 'react';
import { AppState, Button, Linking, Text, View } from 'react-native';

export function ReminderSettingsCard() {
  const [enabled, setEnabled] = useState<boolean | null>(null);

  useEffect(() => {
    const refresh = async () => {
      const current = await Notifications.getPermissionsAsync();
      setEnabled(current.granted);
    };
    refresh();
    const sub = AppState.addEventListener('change', (state) => {
      if (state === 'active') refresh();
    });
    return () => sub.remove();
  }, []);

  if (enabled !== false) return null;
  return (
    <View>
      <Text>Reminders are off. Turn them on to keep your streak.</Text>
      <Button title="Open Settings" onPress={() => Linking.openSettings()} />
    </View>
  );
}

go deeper

for a junior

Know that after a hard denial only the Settings app can turn notifications back on, and that Linking.openSettings() opens it at the app's page.

for a middle

Explain canAskAgain and never_ask_again, why iOS denials are always hard, and why the app re-reads the state with getPermissionsAsync when it becomes active.

for a senior

Design the full denial path: soft versus hard, a cooldown on re-asks, a discreet Settings prompt where the feature is missed, a live re-check, and a working app without notifications.

for a principal

Set the policy for how often and where the product may re-ask, and make sure denial rates by platform and entry point are visible to the people deciding it.

## Soft denial versus hard denial Not every "no" is final, and the code path depends on which kind you have. | Situation | expo-notifications | Core `PermissionsAndroid` | Can a request show a dialog? | |---|---|---|---| | Never asked | `status: 'undetermined'`, `canAskAgain: true` | not requested yet | Yes | | Denied, Android can still ask | `canAskAgain: true` | `'denied'` | Yes | | Denied, Android stopped asking | `canAskAgain: false` | `'never_ask_again'` | No | | Denied on iOS | `status: 'denied'`, `canAskAgain: false` | not applicable | No | | Turned off later in Settings | `status: 'denied'` | `check()` resolves `false` | Depends on the platform | Two details are easy to miss: - On **iOS** the alert is shown once, so any denial is already a hard one. expo-notifications computes `canAskAgain` as "status is not denied". - On **Android 13+**, expo-notifications reports `denied` whenever notifications are disabled for the app, even if the runtime permission itself was once granted, because it also checks whether notifications are enabled. ## When the system can still ask A soft denial is a signal about timing, not a verdict. Do not re-request immediately. Wait for another in-context moment, such as the user setting a second habit reminder, show your own explainer again, and only then call `Notifications.requestPermissionsAsync()` or `PermissionsAndroid.request(...)`. Keep a cooldown so the offer does not reappear on every launch. ## When only Settings can help Once a request can no longer show anything, the app's job is to make the Settings route easy: 1. Show an in-app card or banner where the missing feature is felt, for example on the habit screen: "Reminders are off. Turn them on in Settings." 2. Offer a button that calls `Linking.openSettings()`. React Native's `Linking` opens the Settings app at the app's own settings page. 3. On Android, `Linking.sendIntent('android.settings.APP_NOTIFICATION_SETTINGS', [{ key: 'android.provider.extra.APP_PACKAGE', value: packageName }])` can open the notification screen for the app directly; the React Native docs use this action as their `sendIntent` example. 4. Never call the request API as the button's action: it would resolve without a dialog and look broken. Where the button lands differs by platform. On iOS the app's page in Settings shows a Notifications entry the user taps into; on Android the app's details page lists Notifications among other settings, which is why the direct notification-settings intent saves a step there. Either way, the in-app copy should say exactly which switch to turn on, because the user is leaving the app to do it. ## Coming back from Settings The user may flip the switch and return, or return without changing anything. The app cannot get a callback from Settings, so it re-reads the state when it becomes active: - subscribe with `AppState.addEventListener('change', ...)`; - when the state becomes `'active'`, call `Notifications.getPermissionsAsync()` again; - update the UI and, if now granted, schedule the pending reminders. The same re-check also catches the reverse case: a user who granted permission months ago and has since switched notifications off. Permission is **live state owned by the OS**, not a boolean to store once. ## Measuring the path Denials are only useful to the team if they are visible. Two cheap signals help: - record which state the user was in when the Settings card was shown (soft or hard denial, platform); - record whether a later foreground check found the permission granted, which tells you whether the Settings route actually works for your users. These numbers answer the practical question of whether the explainer, the timing or the Settings card needs work, without guessing. ## Degrade, do not block A denial should remove one capability, not the app. In a habit tracker: - the habit list, streaks and history keep working; - reminders can appear in-app when the user opens it; - the Settings card stays discreet and dismissible. ## Common mistakes - Treating `'denied'` on Android as final when `canAskAgain` is still true. - Calling `requestPermissionsAsync()` from the "Enable" button after a hard denial. - Caching `granted: true` at install and never re-checking. - Blocking the whole app behind a "please allow notifications" wall.

  • Why does the Enable button call Linking.openSettings() instead of requestPermissionsAsync() after a hard denial?
    After a hard denial the system will not draw its dialog, so `requestPermissionsAsync()` resolves at once with the same denied state and the button looks broken. `Linking.openSettings()` takes the user to the app's settings page, the one place where the switch can still be turned on.
  • How does a React Native app notice that the user turned notifications back on in Settings?
    It cannot receive a callback from Settings, so it re-reads the state when it returns to the foreground: an `AppState` change listener calls `Notifications.getPermissionsAsync()` whenever the state becomes `'active'`, then updates the UI and schedules any reminders that were waiting.

saying these in an interview costs you the question

  • A denied notification permission can always be requested again later
  • Linking.openSettings() re-shows the permission dialog
  • Once granted, the notification permission never needs to be checked again
  • Settings calls the app back when the user flips the switch
  • A denial means the app should stop working until the user allows it