skip to content

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%

answer

  1. no dialog, quiet delivery
  2. Notification Center only
  3. user decides from the notification
  4. root status says undetermined
  5. read ios.status for PROVISIONAL

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.

solid answer

~40 s

Passing `requestPermissionsAsync({ ios: { allowProvisional: true, ... } })` adds iOS's provisional option: the system grants a trial authorization **without showing any dialog**, and notifications arrive quietly in Notification Center, without banners or sounds, where the user can choose to keep them or turn them off. The trap is how expo-notifications reports it: only full authorization maps to the root `status: 'granted'`; `PROVISIONAL` (and `EPHEMERAL`) come back as `'undetermined'` with `granted: false`, and the real value lives in `ios.status`. So the common registration snippet, "if status is not granted, call `requestPermissionsAsync()`", runs the default request (alert, badge, sound, no provisional) at the next check and puts the full alert in front of the user at an arbitrary moment. Check `ios.status === IosAuthorizationStatus.PROVISIONAL` explicitly, and upgrade to full authorization deliberately, in context.

code

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

export async function canNotify(): Promise<boolean> {
  const settings = await Notifications.getPermissionsAsync();
  return (
    settings.granted ||
    settings.ios?.status === Notifications.IosAuthorizationStatus.PROVISIONAL
  );
}

export async function startQuietTrial() {
  if (await canNotify()) return;
  await Notifications.requestPermissionsAsync({
    ios: { allowAlert: true, allowBadge: true, allowSound: true, allowProvisional: true },
  });
}

go deeper

for a junior

Recall that iOS provisional authorization needs no dialog and delivers notifications quietly to Notification Center.

for a middle

Explain how expo-notifications reports provisional: root status 'undetermined', granted false, and ios.status PROVISIONAL, and why iOS code should read ios.status.

for a senior

Spot the production bug where a generic granted-check re-requests and fires the full alert at startup, and fix it by treating PROVISIONAL as allowed and centralising the full-authorization ask.

for a principal

Weigh reach against interruption: when quiet delivery is enough, when a feature justifies the full ask, and how to measure whether provisional users ever upgrade.

## What provisional authorization is iOS normally requires an app to show the notification alert and get a yes before it can notify. **Provisional authorization** is the exception: the app requests it, and the system grants it immediately **without any dialog**. The price is how notifications are delivered: - they go **quietly to Notification Center**, with no banner and no sound; - the user sees them only when they look there; - from the notification itself, the user can choose to keep receiving them or turn them off. In expo-notifications, provisional is one of the iOS options passed to `requestPermissionsAsync`: ```ts await Notifications.requestPermissionsAsync({ ios: { allowAlert: true, allowBadge: true, allowSound: true, allowProvisional: true }, }); ``` The option maps directly to iOS's provisional authorization option. It applies from iOS 12, and React Native Firebase's messaging docs describe the same behaviour for their `provisional` flag. ## How expo-notifications reports it The result has two layers: | Field | Full authorization | Provisional | Denied | |---|---|---|---| | `status` | `'granted'` | `'undetermined'` | `'denied'` | | `granted` | `true` | `false` | `false` | | `canAskAgain` | `true` | `true` | `false` | | `ios.status` | `AUTHORIZED` | `PROVISIONAL` | `DENIED` | The native module maps only the authorized state to `granted`; every other non-denied state, including `PROVISIONAL` and `EPHEMERAL`, becomes `'undetermined'`. The Expo documentation says to rely on `ios.status` on iOS for exactly this reason, and its own example treats "allowed" as `settings.granted || settings.ios?.status === IosAuthorizationStatus.PROVISIONAL`. ## How a generic check undoes it Many apps copy a registration function shaped like this: 1. read `status` with `getPermissionsAsync()`; 2. if it is not `'granted'`, call `requestPermissionsAsync()`; 3. if it is still not granted, give up. After a provisional grant, step 1 reads `'undetermined'`, so step 2 runs the **default request**, which asks for alert, badge and sound with no provisional flag. Because provisional is not a final decision, iOS can then show the full alert. The result is the opposite of the design: - the quiet trial ends before the user has seen a single reminder; - the full prompt appears at whatever moment the registration code runs, usually app start, with no context; - a refusal there turns a working quiet channel into a hard denial. A variant of the same bug does not prompt but treats `granted: false` as "notifications off", so the habit tracker stops scheduling reminders the user was quietly receiving. ## Using provisional on purpose For a **habit tracker**, provisional delivery fits onboarding: the app can start sending a daily summary without interrupting anyone. It fits reminders less well, because a reminder that arrives without a banner or sound at 8:00 is easy to miss. A sound design is: - request provisional at onboarding and treat `PROVISIONAL` as allowed in every check; - when the user sets a timed reminder or reaches a streak worth protecting, show your explainer and request full authorization without `allowProvisional`; - keep the full-authorization ask in one place so no other code path triggers it by accident. ## Checking the state correctly Every place that asks "may I notify?" should use the same helper, so the provisional case is handled once: - `granted` is true: full authorization, show the reminder normally; - `ios.status` is `PROVISIONAL`: allowed, but quiet; schedule the reminder and consider a later upgrade; - `ios.status` is `NOT_DETERMINED`: never asked; the in-context flow may show the explainer; - `ios.status` is `DENIED`: send the user to Settings instead of requesting. Scattered `status === 'granted'` checks are the usual source of the bug, because each one quietly treats a provisional user as someone who still has to be asked. ## Related states - `EPHEMERAL` is a time-limited authorization used by App Clips; it is also reported as `'undetermined'` at the root. - expo-notifications has no Android counterpart: `allowProvisional` lives under the `ios` key, and on Android the call only passes the `android` part of the request. ## Common mistakes - Reading the root `status` on iOS and concluding the app is not allowed to notify. - Assuming provisional expires on its own after a trial period. - Expecting provisional notifications to appear as banners or play sounds.

  • How should the habit tracker move a provisionally authorized user to full authorization?
    At a moment where interruptions clearly help, such as the user setting a timed reminder, show an explainer and call `requestPermissionsAsync()` without `allowProvisional`. Because provisional is not a final decision, iOS can present the full alert then. Keep that call in one function so no generic startup check triggers it.
  • Why is provisional delivery a weaker fit for timed habit reminders than for a daily summary?
    Provisional notifications land in Notification Center without a banner or sound, so they are seen only when the user looks. A summary loses little by waiting; a reminder meant to fire at 8:00 loses its point. Provisional works as a trial, not as the end state for time-sensitive reminders.

Provisional delivery is like leaving a flyer in the mailbox instead of ringing the doorbell: it arrives without interrupting anyone, and the resident decides later whether future visits may ring. The catch is the same too: a flyer about an 8:00 appointment is easy to miss.

saying these in an interview costs you the question

  • Provisional authorization shows a smaller version of the usual permission dialog
  • expo-notifications reports provisional authorization as granted: true
  • Provisional authorization expires automatically after a trial period
  • Provisional notifications still show banners and play sounds
  • allowProvisional also enables quiet delivery on Android