◉RORK LABJP
●GPT-6.1 SOL — Added to Rork's model menu (09-29) with Low, Medium and High effort; included on Pro and Max●SONNET 5.5 — Claude Sonnet 5.5 is selectable: strong at UI design and faster than Sonnet 5 (09-28)●SONNET 4.5 — Retires on the Claude API on 11/30, 27 days left. If your app calls it, check the migration target●SDK 58 — Moves to React Native 0.88. R8 is now on by default in Android release builds, and iOS prebuild generates a SceneDelegate●OBSERVE — EAS Observe now records native crashes too (SDK 57.0.21 or later, 10/07)●NEW — My App Open Ad Only Showed When the App…●GPT-6.1 SOL — Added to Rork's model menu (09-29) with Low, Medium and High effort; included on Pro and Max●SONNET 5.5 — Claude Sonnet 5.5 is selectable: strong at UI design and faster than Sonnet 5 (09-28)●SONNET 4.5 — Retires on the Claude API on 11/30, 27 days left. If your app calls it, check the migration target●SDK 58 — Moves to React Native 0.88. R8 is now on by default in Android release builds, and iOS prebuild generates a SceneDelegate●OBSERVE — EAS Observe now records native crashes too (SDK 57.0.21 or later, 10/07)●NEW — My App Open Ad Only Showed When the App…
Articles/Dev Tools
⬡ Dev Tools/2026-06-14Intermediate

Stop Burning Your One Push-Permission Shot on App Launch — Pre-Prompt Priming for Rork Apps

If your Rork (Expo) app fires the OS push-permission dialog at launch, every 'Don't Allow' tap closes that channel forever — iOS won't let you ask again. Here's how a self-built pre-permission screen lifts your opt-in rate, with the Expo code to do it.

Rork611Push Notifications9Expo234Retention13Solo Dev3

The first thing I quietly lost after shipping a Rork-built app was push permission. I had wired it so the OS dialog — "'App' would like to send you notifications" — appeared the instant the app opened. Faced with that on first launch, most users tapped "Don't Allow" without a second thought.

The nasty part: on iOS, once someone taps "Don't Allow," your app can never bring that dialog back. Even if the value of notifications becomes obvious later, the user has to dig into the Settings app and flip the switch themselves. Almost nobody does. So if you get that single first ask wrong, you lose the notification channel to that user permanently. Running wallpaper and relaxation apps for years, I learned that simply not leaving that "first ask" up to the OS moves the opt-in rate more than anything else. Here's the design.

Why "Right at Launch" Wastes the Most

The OS push dialog comes with two constraints you can't touch. You can't change its wording, and it's a binary choice — "Allow" or "Don't Allow" — where the second answer ends the conversation for good.

First launch is the moment when the user hasn't experienced anything in your app yet. Asking "may I send notifications?" before they've felt any value gives them nothing to decide with. When people hesitate, they pick the option that feels safer: "Don't Allow." That isn't users being cold — it's the natural response to a binary demand made without any context.

This is where pre-permission priming helps. Before the OS dialog, you show your own screen that explains, just once, why notifications are useful, and you only forward people to the OS dialog after they've thought "yeah, I might want that." Your own screen can appear as many times as you like, with whatever copy you choose. Show the OS dialog only to people who warmed up here, and you dramatically cut the instant-death "Don't Allow" taps.

The Core Idea: Demote the OS Dialog to a Final Confirmation

The trick is to flip the framing. Instead of treating the OS dialog as the first gate, position it as the last confirmation for someone who has already made up their mind.

The flow looks like this. First, your own priming screen makes the case. Anyone who taps "Later" there does not see the OS dialog (showing it would spend your one precious shot). Only people who tap "Turn on notifications" get the OS dialog — and since they're already on board by then, it sails through.

The point I most want to land here is this: splitting it into two steps isn't a numbers trick for inflating opt-in rates. The real value is that you give the user's freedom to decline a safe place to land — the "Later" button on your own screen. A decline at the OS dialog can never be retried; a "Later" on your screen can be re-asked when a better context comes along. Moving the decline to a recoverable place is the heart of the design.

Implementing It in Expo

Rork generates apps on Expo (React Native) under the hood. Push permission is handled by expo-notifications. The first rule: only call the permission request from the "Turn on notifications" button on your own screen.

The helper below checks the current permission state and shows the OS dialog only if we've never asked. For people who are already denied, it skips the dialog and signals that we need to route them to Settings instead.

import * as Notifications from 'expo-notifications';
import { Linking } from 'react-native';
 
type PrimeResult = 'granted' | 'denied' | 'needs-settings';
 
// Call this when "Turn on notifications" is tapped on your priming screen
export async function requestPushAfterPriming(): Promise<PrimeResult> {
  const current = await Notifications.getPermissionsAsync();
 
  // Already granted — do nothing
  if (current.status === 'granted') {
    return 'granted';
  }
 
  // On iOS, once denied, requestPermissionsAsync won't show a dialog —
  // it returns denied immediately. Routing to Settings is the only path.
  if (current.status === 'denied' && !current.canAskAgain) {
    return 'needs-settings';
  }
 
  // Only show the OS dialog while still undetermined
  const result = await Notifications.requestPermissionsAsync();
  return result.status === 'granted' ? 'granted' : 'denied';
}
 
// Open the Settings app (for the needs-settings case)
export function openNotificationSettings() {
  Linking.openSettings();
}

The crucial bit is the canAskAgain check. On iOS, once a user has declined, status becomes denied and canAskAgain becomes false, and calling requestPermissionsAsync() shows nothing. If you don't know this and just write "not granted, so let's ask again" loops, you fall into a silent failure: the user sees nothing while your code keeps getting denied on every call. I missed this at first and spent a while puzzled over why my opt-in rate wouldn't budge.

The priming screen that calls it can be as simple as a value statement plus two buttons, "Turn on notifications" and "Later":

import { View, Text, Pressable } from 'react-native';
import { requestPushAfterPriming } from './push';
 
export function PushPrimingScreen({ onDone }: { onDone: () => void }) {
  const handleAllow = async () => {
    const result = await requestPushAfterPriming();
    // Whether granted or denied, continue the normal flow from here
    onDone();
  };
 
  return (
    <View style={{ flex: 1, justifyContent: 'center', padding: 24 }}>
      <Text style={{ fontSize: 22, fontWeight: '700', marginBottom: 12 }}>
        We'll let you know when new wallpapers arrive
      </Text>
      <Text style={{ fontSize: 15, lineHeight: 22, color: '#555', marginBottom: 32 }}>
        A few times a week, just the new picks our editors love. You can change the frequency anytime in settings.
      </Text>
 
      <Pressable onPress={handleAllow} style={{ backgroundColor: '#111', padding: 16, borderRadius: 12, alignItems: 'center' }}>
        <Text style={{ color: '#fff', fontWeight: '600' }}>Turn on notifications</Text>
      </Pressable>
 
      <Pressable onPress={onDone} style={{ padding: 16, alignItems: 'center', marginTop: 8 }}>
        <Text style={{ color: '#888' }}>Later</Text>
      </Pressable>
    </View>
  );
}

Notice that tapping "Later" never calls requestPushAfterPriming. Because you don't fire the OS dialog here, you keep the chance to try again another day.

When to Ask — Timing Beats Copy

Before you polish the wording, fix the timing. In my experience, the ask lands best not at launch but right after the user has had one good experience in the app.

For a wallpaper app, that's the moment they've actually finished setting their first image as the device wallpaper. For a relaxation app, it's right after they've finished their first session and feel good. In other words, you aim for the moment when the value of notifications connects naturally to what just happened: "when a new piece arrives, you can have this experience again." That bridge forms on its own in the user's head.

The three moments to avoid: right at launch, right after a paywall, and right after an error. Launch has no context, post-paywall feels like "another demand," and post-error simply leaves a bad taste.

Recovering After a Decline

For people who tapped "Later" on your own screen, re-ask a fixed number of times, well spaced out. My rule of thumb is up to three times, with plenty of gap between each. Instead of repeating the same screen, anchor each ask to something that actually happened ("a sequel to the piece you saved was just published"); that makes it feel less pushy.

For people who reached denied at the OS level, place a single, unobtrusive "Turn on notifications" entry point somewhere in the app, and let it open the Settings app via Linking.openSettings(). The important thing is not to nag. Repeatedly pushing someone who already declined at the OS level only erodes the experience for little return — that's what years of operating these apps have taught me.

Done well, notifications become a quiet ally for retention; done wrong, they're a fragile channel where one bad first ask ends the relationship. Demote the OS dialog to a final confirmation, and keep the freedom to decline inside your own screen. That single bit of care should noticeably cut what you lose. I hope it helps anyone stuck at the same spot.

Share

Thank You for Reading

Rork Lab is ad-free, supported entirely by members like you. We publish practical guides daily with implementation code, benchmarks, and production-ready patterns. If you've found it useful, we'd love to have you on board.

  • ✦Copy-paste ready implementation code
  • ✦New advanced guides published daily
  • ✦$5/mo or $10 for lifetime access
View Membership →

If you found this article helpful, a small tip ($1.50) would mean a lot to us. Your support helps keep this site ad-free and covers server and hosting costs.

Related Articles

⬡ Dev Tools2026-07-17
Shipping Notifications Without Asking First — Provisional Authorization in Rork Apps, and the Expo Snippet That Quietly Undoes It
iOS lets you start delivering notifications with no permission dialog at all, via provisional authorization. The catch: expo-notifications reports granted as false for provisional devices, so the registration snippet in Expo's own docs re-requests permission and fires the very dialog you were avoiding. Here's why granted lies, a hook that models authorization as five states, how to write notifications for quiet delivery, when to ask for the upgrade, and how to keep provisional out of your CTR.
⬡ Dev Tools2026-10-30
It runs in development but the iOS Release build dies on launch — checking whether a missing scheme is the cause
In an expo-router app, an app.json with no scheme can crash only the iOS Release build at launch. Why Debug and Android survive, the two ways to fix it, how to think about leaving the scheme out on purpose, and a pre-submission check.
⬡ Dev Tools2026-10-15
When EXPO_PUBLIC_ Values Read Fine in Dev but Come Back undefined in Production: Telling the Spellings Apart
The API URL reads fine while developing and turns undefined once the app is built. The cause was how process.env was written. I lined up babel-preset-expo's output for dev and production and tabulated which spellings get replaced and which are left behind.