Famitha M AStop asking for push permission on install. A session counter, a habit-moment trigger, and a recovery flow in about 20 lines.
TL;DR
Linking.openSettings()
On iOS you get one native permission prompt. Decline it and the OS will never show it again for your app. The user has to dig through Settings to reverse it, which almost nobody does.
So the question is not "how do I ask." It is "when does the user have a reason to say yes."
At install, they don't. They opened your app forty seconds ago. They have no idea what your notifications contain, whether they are useful, or how often they will fire. Blind install-time prompts get rejected constantly, and every rejection is permanent. You are spending an unrecoverable resource at the moment of lowest trust.
Not "day three." Not "72 hours after install." Session three: the third distinct time the user opens the app. By then they have voluntarily returned twice. They have context. The ask is no longer coming from a stranger.
The mechanic is a counter that increments once per cold start:
import AsyncStorage from '@react-native-async-storage/async-storage';
const SESSION_KEY = 'session_count';
export async function bumpSession() {
const raw = await AsyncStorage.getItem(SESSION_KEY);
const count = (parseInt(raw, 10) || 0) + 1;
await AsyncStorage.setItem(SESSION_KEY, String(count));
return count;
}
Call it once from your root component:
useEffect(() => {
bumpSession().then(setSessionCount);
}, []);
Then gate the prompt:
import * as Notifications from 'expo-notifications';
async function maybeAskForPush(sessionCount) {
if (sessionCount < 3) return;
const { status } = await Notifications.getPermissionsAsync();
if (status !== 'undetermined') return; // already asked, never re-burn
await Notifications.requestPermissionsAsync();
}
The undetermined check matters. It makes the function safe to call on every launch without ever re-triggering anything.
Session three is the floor, not the ceiling. A raw counter still fires the prompt at an arbitrary moment. The stronger pattern is tying the ask to the first action that notifications will actually serve:
At that moment the value proposition is self-evident. "Want a reminder when this is due?" needs no explanation. If your app has a moment like that, use it as the trigger and keep the session count as a fallback for users who never hit it:
async function askAtHabitMoment() {
const { status } = await Notifications.getPermissionsAsync();
if (status === 'undetermined') {
await Notifications.requestPermissionsAsync();
}
}
Same guard, different call site. That is the whole change.
If you are prototyping this flow, this gating logic is exactly the kind of glue code worth generating rather than hand-writing. RapidNative scaffolds React Native and Expo screens from a prompt, and wiring a permission flow like this into a generated onboarding is a five-minute job.
The popular advice says: show a custom in-app modal first ("We'd like to send you helpful reminders!"), and only fire the native prompt if the user taps yes.
The theory is sound. In practice it is easy to get wrong, and plenty of teams have watched it hurt more than help. You are now showing two interruptions instead of one, and the custom modal gives users a free, zero-cost place to say no. Some users who would have shrugged and accepted the native prompt bounce off the softer one first.
If you ask at a habit moment, the context does the explaining and the extra modal is redundant. My take: ship without it, and only add it if your own funnel data says otherwise. Do not cargo-cult it in.
Already burned the prompt on install? You cannot re-trigger it, but you can route users to Settings at a habit moment instead:
import { Linking } from 'react-native';
async function recoverPush() {
const { status, canAskAgain } = await Notifications.getPermissionsAsync();
if (status === 'undetermined' || canAskAgain) {
await Notifications.requestPermissionsAsync();
} else if (status === 'denied') {
// one-tap jump to your app's own settings page
Linking.openSettings();
}
}
Pair Linking.openSettings() with a single line of UI ("Turn on reminders in Settings") and fire it only at a moment where the user just tried to do something that needs notifications. Cold recovery rates are low, but warm ones, triggered by intent, are worth shipping.
Counter, guard, trigger, recovery. About 20 lines total, no library beyond AsyncStorage and expo-notifications, and it protects the one prompt you cannot get back.
What trigger moment are you using in your app: session count, habit moment, or something weirder? Drop it in the comments.