skip to content

Consent Prompt Timing

iOS asks for notification authorization with options such as provisional delivery, and Android 13 added the POST_NOTIFICATIONS runtime permission. Interviewers probe when to ask and handling denial.

part ofReact Nativeoverview, primer and where to startread it →
on this pageshow

explore

questions

6

In a React Native app, how does asking for push notification permission differ between iOS and Android 13 or later?

level: juniorimportance: must knowfreq 62%

answer

  1. one system alert, shown once
  2. Android default-on before API 33
  3. POST_NOTIFICATIONS: declare, then request
  4. iOS needs no usage string
  5. status, granted, canAskAgain

basics

~20 s

iOS requires every app to request notification authorization through a one-time system alert. Android showed notifications by default until Android 13 (API 33) added the POST_NOTIFICATIONS runtime permission, which must be declared in the manifest and requested.

solid answer

~40 s

On iOS the app asks the notification center for authorization with a set of options (alert, badge, sound, optionally provisional); the system alert appears once, and later requests resolve silently with the stored answer. No Info.plist usage string is needed. On Android 12 and below there is nothing to ask for: notifications are on at install and the user can only switch them off in Settings. Android 13 (API 33) added the `POST_NOTIFICATIONS` runtime permission, so the app must declare it and request it like any other dangerous permission. React Native 0.87's default Android build targets API 36, so a build on the default configuration is subject to it. In an Expo app, `Notifications.requestPermissionsAsync()` from expo-notifications covers both: iOS options go under its `ios` key, and on Android 13+ it shows the `POST_NOTIFICATIONS` dialog.

code

typescript · 12 lines
typescript
import * as Notifications from 'expo-notifications';

export async function ensureReminderPermission(): Promise<boolean> {
  const current = await Notifications.getPermissionsAsync();
  if (current.granted) return true;
  if (!current.canAskAgain) return false;

  const answer = await Notifications.requestPermissionsAsync({
    ios: { allowAlert: true, allowBadge: true, allowSound: true },
  });
  return answer.granted;
}

go deeper

for a junior

Recall the split: iOS always asks once through a system alert, Android asks only from Android 13 through POST_NOTIFICATIONS. Know the expo-notifications pair getPermissionsAsync and requestPermissionsAsync.

for a middle

Explain what the result object carries (status, granted, canAskAgain, ios.status), why a second iOS request shows nothing, and why the Android permission must also be declared in the merged manifest.

for a senior

Show you design for both states in one flow: old Android devices with nothing to ask, Android 13+ with a one-or-two-shot dialog, iOS with a single alert, and Settings as the only route back after a refusal.

for a principal

Frame notification consent as a scarce, platform-owned asset: the team decides when the one real ask happens, how the app behaves without it, and how the permission state is measured across both platforms.

## Two platforms, two permission models A React Native app shows notifications through each platform's own notification system, so it inherits two different consent models. Neither is a React Native invention: the JavaScript code only asks a native module to run the platform's request and reports the answer back. | Aspect | iOS | Android 12 and below | Android 13+ (API 33+) | |---|---|---|---| | Consent needed before showing | Yes, always | No, on at install | Yes, runtime permission | | What is asked | Authorization with options | Nothing | `POST_NOTIFICATIONS` | | Declaration | No Info.plist usage string | None | `<uses-permission>` in the manifest | | Asking again after a "no" | Resolves with no dialog | Not applicable | Dialog stops after repeated denials | | Where the user reverses it | Settings app | Settings app | Settings app | ## iOS: authorization with options On iOS the app requests **authorization** from the user notification center and passes a set of **options** saying what it wants to do. In expo-notifications these are fields of the `ios` object passed to `requestPermissionsAsync`: - `allowAlert`, `allowBadge`, `allowSound` — the everyday trio, and exactly what the call requests when you pass no argument at all; - `allowProvisional` — quiet, prompt-free delivery to Notification Center; - `allowCriticalAlerts`, `allowDisplayInCarPlay`, `provideAppNotificationSettings` — specialised options most apps never set. The system alert is shown **once**. After the user answers, a new request resolves immediately with the stored decision, so a denial can only be reversed by the user in the Settings app. Unlike the camera or location, notifications need **no usage-description string** in Info.plist; the Expo documentation states this explicitly. ## Android: from default-on to a runtime permission Before Android 13, notifications were simply enabled when the app was installed. The user could disable them in Settings, but the app had nothing to request. Android 13 (API level 33) introduced `POST_NOTIFICATIONS` as a **runtime permission**: 1. The permission must be **declared** in the merged AndroidManifest. expo-notifications declares it in its own library manifest, so an Expo app gets it automatically; a bare app using another library may have to add it by hand. 2. The app must **request** it at runtime, which shows the system dialog. 3. If the user keeps refusing, Android stops showing the dialog and the only path left is Settings. The rule applies to apps that target API 33 or higher, and React Native 0.87's Gradle setup targets API 36 by default, so any build on that default is affected. On a device running Android 12 or below, the same request code has nothing to show: expo-notifications' `requestPermissionsAsync` simply returns the current enabled/disabled state. ## One call in React Native code In an Expo project the whole thing is one API: - `Notifications.getPermissionsAsync()` reads the current state without any user-facing effect. - `Notifications.requestPermissionsAsync(request?)` shows the platform's prompt where one exists and resolves with the new state. In a bare React Native app without expo-notifications, the Android half is available in core as `PermissionsAndroid.request(PermissionsAndroid.PERMISSIONS.POST_NOTIFICATIONS)`, while iOS needs a library that wraps the native authorization call. React Native Firebase's messaging module still ships a `requestPermission` method, but it is deprecated in v26 and does nothing on Android. ## Reading the answer expo-notifications resolves a `NotificationPermissionsStatus` object: - `status` — `'granted'`, `'denied'` or `'undetermined'`; - `granted` — a boolean shortcut for `status === 'granted'`; - `canAskAgain` — whether another request can still show a dialog; - `ios.status` — the finer iOS state (`NOT_DETERMINED`, `DENIED`, `AUTHORIZED`, `PROVISIONAL`, `EPHEMERAL`), which should be read instead of the root `status` on iOS; - `android.importance` — the app-level importance Android reports. ## Where each piece lives in a project The same consent flow touches different files depending on the project type, which is a frequent follow-up in interviews: | Piece | Expo project (prebuild) | Bare React Native project | |---|---|---| | Android declaration | Merged in from expo-notifications' library manifest | Whatever library you use, or a line in `android/app/src/main/AndroidManifest.xml` | | Android request | `requestPermissionsAsync()` | `PermissionsAndroid.request(...)` or a library | | iOS request | `requestPermissionsAsync({ ios: {...} })` | A library that wraps the native authorization call | | iOS declaration | None needed | None needed | Because the declaration is merged at build time and the request happens at runtime, a bug in either half shows up only on a real Android 13+ device, never in JavaScript tests. ## The answer is not permanent On both platforms the user can change their mind later in the Settings app, in either direction. That is why the result of a request is best treated as a snapshot: apps re-read it with `getPermissionsAsync()` when it matters, for example before scheduling a reminder or when the app returns to the foreground, instead of storing a boolean once at install. ## Common mistakes - Assuming Android needs no permission at all, which was true only before Android 13. - Asking on iOS a second time and expecting a second dialog. - Hunting for an Info.plist usage string that iOS does not require for notifications. - Treating the Android request as a no-op everywhere because it was one on older devices.

  • What happens if a React Native app on Android 13 requests POST_NOTIFICATIONS but the permission is not declared in the manifest?
    The request is refused without any dialog, because Android only grants runtime permissions that the merged manifest declares. expo-notifications declares `POST_NOTIFICATIONS` in its own library manifest, so Expo apps get it through manifest merging; a bare app that asks through `PermissionsAndroid` must make sure some manifest in the build declares it, usually by adding the `uses-permission` line to its app manifest.
  • In expo-notifications, why should iOS code read ios.status rather than the root status field?
    The root `status` collapses iOS's finer states: only full authorization maps to `'granted'`, while provisional and ephemeral authorization come back as `'undetermined'`. `ios.status` keeps the real value from `IosAuthorizationStatus`, so code can tell a quietly authorized app from one that has never asked.

saying these in an interview costs you the question

  • Android never needs a notification permission, it is always on
  • iOS will show the notification prompt again on the next request
  • iOS needs a usage-description string in Info.plist for notifications
  • A permission request on Android 12 shows the same dialog as Android 13
  • React Native has its own notification permission independent of the OS
open as a page

In a React Native habit-tracker app, when should you trigger the notification permission prompt for reminders, and why not at first launch?

level: middleimportance: must knowfreq 55%

basics

~20 s

Trigger it when the user asks for something that needs it, such as setting their first habit reminder, after a short in-app explanation. A cold prompt at launch gets refused, and iOS shows its alert only once.

open as a page

With expo-notifications on Android 13 or later, why must a notification channel exist before you request notification permission?

level: middleimportance: should knowfreq 30%

basics

~10 s

Expo's documentation states that on Android 13 the notification permission prompt will not appear until at least one channel exists. So call setNotificationChannelAsync before requestPermissionsAsync and before fetching a push token.

open as a page

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%

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.

open as a page

In a bare React Native app using React Native Firebase v26 messaging, why might Android 13 users never see a notification prompt, and how do you fix it?

level: seniorimportance: should knowfreq 28%

basics

~10 s

React Native Firebase's messaging requestPermission() is iOS-only: on Android it resolves AUTHORIZED without asking, and it is deprecated. Declare POST_NOTIFICATIONS in the app manifest and request it with PermissionsAndroid, or use expo-notifications.

open as a page

With expo-notifications on iOS, what does allowProvisional do, and why can checking only the root status field undo it?

level: seniorimportance: should knowfreq 25%

basics

~20 s

allowProvisional asks iOS for provisional authorization: no dialog, notifications delivered quietly to Notification Center. expo-notifications reports that as root status 'undetermined', so a generic granted-check fires the full prompt and throws the quiet trial away.

open as a page