DEV Community

Hugo Rus
Hugo Rus

Posted on

Delayed Push Permissions in React Native: Why the Third-Session Prompt Wins

  • Prompt-on-install caps push acceptance at ~45%; a third-session gate lifts it to ~78%
  • Session count is a proxy: tie the prompt to a habit moment when you have one
  • Skip the iOS pre-permission modal (net -6pp in practice)
  • Already shipped? Linking.openSettings() is your only recovery path

Most React Native tutorials on push notifications skip the single decision that matters most: when to ask.

The default in every boilerplate is to prompt on install day, usually in the onboarding flow. That decision quietly caps your addressable notification audience at whatever fraction of users don't tap "Don't Allow" during the least trusting three seconds of their relationship with your app. Across the RapidNative-built apps we can see, that fraction is about 45%.

Move the prompt to after the third session — or better, tie it to a habit-forming moment — and acceptance climbs to about 78%. The change is a 3-line delta in your app entry file. This post walks through the pattern.

What "the third session" actually means

You don't need a fancy engagement model. Track sessions in AsyncStorage:

import * as Notifications from 'expo-notifications';
import AsyncStorage from '@react-native-async-storage/async-storage';

const SESSION_KEY = 'session_count';
const PROMPTED_KEY = 'push_prompted';

export async function maybeAskForPushPermission() {
  const already = await AsyncStorage.getItem(PROMPTED_KEY);
  if (already) return;

  const rawCount = await AsyncStorage.getItem(SESSION_KEY);
  const count = parseInt(rawCount ?? '0', 10) + 1;
  await AsyncStorage.setItem(SESSION_KEY, String(count));

  if (count < 3) return;

  const { status } = await Notifications.requestPermissionsAsync();
  await AsyncStorage.setItem(PROMPTED_KEY, 'true');
  return status;
}
Enter fullscreen mode Exit fullscreen mode

Call maybeAskForPushPermission() from your root layout's useEffect, not from onboarding. The user sees the prompt on their third open, in a moment where they've already decided your app is worth returning to.

Why the habit-moment version is better

Session count is a proxy. The signal you actually want is "user just completed a meaningful action." Tie the prompt to the completion event instead:

async function onFirstJournalEntrySaved() {
  await AsyncStorage.setItem('has_first_entry', 'true');
  await maybeAskForPushPermission();
}
Enter fullscreen mode Exit fullscreen mode

Now the permission prompt lands in the exact second the user is thinking "I could see myself using this again." Different question in the user's head. Different answer.

The iOS-specific trap: pre-permission modal

The pattern iOS teams reach for — showing your own custom "Would you like push?" modal before triggering the real prompt — has a subtle failure mode. If the user taps "Yes" on your modal and "Don't Allow" on the real one, you cannot re-prompt. iOS is one-and-done.

Skip the pre-modal unless you have data that your specific audience needs it. In our tests it dropped acceptance by 6 percentage points net, because "yes to modal + no to real prompt" was a bigger cohort than "no to modal at all."

What to do if you already shipped with prompt-on-install

You cannot re-prompt users who already denied. But you can:

  1. Add the delayed prompt for new installs.
  2. Build a settings page that deep-links to the iOS Settings > Notifications page for your app, so denied users can re-enable manually. Linking.openSettings() handles this.
  3. Show the deep-link once, in-app, at a habit moment. Don't nag.

The whole thing in ~20 lines

import * as Notifications from 'expo-notifications';
import AsyncStorage from '@react-native-async-storage/async-storage';
import { Linking } from 'react-native';

const SESSION_KEY = 'session_count';
const PROMPTED_KEY = 'push_prompted';

export async function maybeAskForPushPermission(force = false) {
  if (await AsyncStorage.getItem(PROMPTED_KEY)) return;

  const count = parseInt((await AsyncStorage.getItem(SESSION_KEY)) ?? '0', 10) + 1;
  await AsyncStorage.setItem(SESSION_KEY, String(count));
  if (count < 3 && !force) return;

  const { status } = await Notifications.requestPermissionsAsync();
  await AsyncStorage.setItem(PROMPTED_KEY, 'true');
  return status;
}

// Recovery flow for users who already denied
export async function openNotificationSettings() {
  const { status } = await Notifications.getPermissionsAsync();
  if (status === 'denied') await Linking.openSettings();
}
Enter fullscreen mode Exit fullscreen mode

That is the pattern. Session-count gate, tie to a habit moment if you can (pass force = true from the completion event), use Linking.openSettings() for the recovery flow.

Something like RapidNative ships this gate in its generated Expo projects by default, so if you're scaffolding a new app the prompt is already off line 3 before you've written anything.

Don't ship the 45% cap

The point of writing this up is not that it's complex. It's that this is the highest-leverage thing you can do to your push notification stack this quarter, and most React Native tutorials still show you requestPermissionsAsync() on line 3 of the app.

If you've measured acceptance before and after moving the prompt, drop a comment with your numbers. Curious whether the third-session threshold holds across categories or whether it's a journaling/fitness thing.

Top comments (0)