RORK LABJP
TOOLING — Rork's developer repos keep moving: rork-xcode was updated on July 16, rork-device on July 15, and rork-plist on July 13OPUS46 — Claude Opus 4.6 is live in Rork, and Rork Max is built to assemble apps on top of Claude CodeSIM — A cloud iOS simulator runs in the browser, with one click to install on a device and two clicks to publish to the App StoreMAX — Rork Max emits pure Swift rather than React Native, reaching iPhone, iPad, Apple Watch, Apple TV, Vision Pro, and even iMessageNATIVE — That opens up HealthKit, ARKit and LiDAR, NFC, Dynamic Island, Live Activities, 3D through Metal, and on-device inference with Core MLSEED — Rork raised a $15M seed led by Left Lane Capital, with Peak XV and a16z Speedrun joining the roundTOOLING — Rork's developer repos keep moving: rork-xcode was updated on July 16, rork-device on July 15, and rork-plist on July 13OPUS46 — Claude Opus 4.6 is live in Rork, and Rork Max is built to assemble apps on top of Claude CodeSIM — A cloud iOS simulator runs in the browser, with one click to install on a device and two clicks to publish to the App StoreMAX — Rork Max emits pure Swift rather than React Native, reaching iPhone, iPad, Apple Watch, Apple TV, Vision Pro, and even iMessageNATIVE — That opens up HealthKit, ARKit and LiDAR, NFC, Dynamic Island, Live Activities, 3D through Metal, and on-device inference with Core MLSEED — Rork raised a $15M seed led by Left Lane Capital, with Peak XV and a16z Speedrun joining the round
Articles/App Dev
App Dev/2026-06-03Intermediate

Unifying Onboarding Across Six Wallpaper Apps: What One Month of First-Day Retention Showed Me

I folded the onboarding flows of six wallpaper apps scaffolded with Rork into a single config-driven component and watched first-day retention and push opt-in for a month. Here is an honest, operational note on what moved and what didn't.

Rork515Onboarding8Wallpaper3Retention13Expo149React Native209

When you run six wallpaper apps in parallel, you eventually notice that you have rebuilt the same "first three screens" six times. The palette and the copy drift apart, and you lose track of which app got which fix. In May 2026 I finally collapsed all of it into one shared onboarding flow, ran it for a month, and wrote down honestly how the numbers moved before and after.

Why I rebuilt all six at once

The trigger wasn't revenue or traffic. It was the pain of maintenance. I would fix the wording on the second onboarding screen in one app and leave it stale in another. The moment I asked for push permission also varied per app—"right at launch" here, "after the first screen" there—so even when I compared opt-in rates, I couldn't isolate what actually worked.

Both of my grandfathers were temple carpenters, and I grew up watching how a carefully fitted joint lasts for decades. Code is the same: six near-identical-but-not-quite implementations look convenient and are actually the most fragile state you can be in. I decided to start by running a single structure through all of them.

What I did to unify them

The approach was simple: instead of sharing the screens themselves, I passed the contents of onboarding as configuration. Each app declares only its palette, copy, and step count; the rendering logic and the timing of the push-permission call live in one shared component.

// onboarding.config.ts —— the only thing that changes per app
export type OnboardingStep = {
  key: string;
  title: string;
  body: string;
  image: number; // the return value of require()
  askPushAfter?: boolean; // ask for permission right after this screen?
};
 
export const onboardingConfig: OnboardingStep[] = [
  {
    key: "welcome",
    title: "A quiet wallpaper for every day",
    body: "Dress your screen in a mood-matched wallpaper in a few taps.",
    image: require("./assets/onb-welcome.webp"),
  },
  {
    key: "value",
    title: "Your favorites are one tap away",
    body: "Saved wallpapers gather at the top of the home screen.",
    image: require("./assets/onb-value.webp"),
    askPushAfter: true, // ask for notifications only after value lands
  },
];
// SharedOnboarding.tsx —— identical across all six apps
import { useState, useCallback } from "react";
import * as Notifications from "expo-notifications";
import { onboardingConfig } from "./onboarding.config";
 
export function SharedOnboarding({ onDone }: { onDone: () => void }) {
  const [index, setIndex] = useState(0);
  const step = onboardingConfig[index];
 
  const next = useCallback(async () => {
    if (step.askPushAfter) {
      // show the OS dialog only after the user understands the value
      await Notifications.requestPermissionsAsync();
    }
    if (index < onboardingConfig.length - 1) {
      setIndex((i) => i + 1);
    } else {
      onDone();
    }
  }, [index, step, onDone]);
 
  return <OnboardingView step={step} progress={(index + 1) / onboardingConfig.length} onNext={next} />;
}

The key is askPushAfter. I used to fire the permission dialog right at launch, but asking before any value has landed just earns a rejection. While unifying, I standardized on "right after the second screen, where the value is clear." Each app now only edits a config file, and the whole class of "forgot to fix the copy" bugs disappears structurally.

Three walls that were harder than expected

I assumed unification would glide along. In practice a few things snagged.

First, the image assets were named differently in every app. The shared component expects the return value of require(), so I had to rename each app's assets/ to a convention like onb-welcome.webp. It's unglamorous, but leaving it vague defeats the point of sharing.

Second, the first-launch check. A few of the six stored their "first run" flag under different key names in older code, and after unification I once shipped a bug where existing users saw onboarding again. I fixed it by adding a small migration that reads the old keys and moves them to the new one.

Third, changing the push-permission timing caused a double dialog in one app, where my own pre-permission screen collided with the OS prompt. I only caught that duplication because I had consolidated the permission call into a single place—which, in the end, made the code easier to read.

What one month of numbers showed

Averaged across my six apps, comparing the four weeks before and after, the change looked roughly like this. These are observations from my own apps; genre and country mix will shift them, so read them with that grain of salt.

Onboarding completion rose from about 71% to about 84%—trimming three screens down to two seems to have helped. First-day retention (D1) went from around 32% to around 38% on a six-app average. Push opt-in climbed from 19%, back when I asked at launch, to 27% after moving the request to right after the value screen. Having shipped apps solo since 2014, seeing a double-digit-percent swing from nothing but permission timing was, honestly, a real takeaway.

A note on measurement: completion is the onDone event reaching the final screen, and first-day retention is a new user reopening the next day. I kept the measurement code untouched across the change, because if you don't pin that down you can never tell later whether the product improved or the metric simply drifted. On the other hand, I saw no clear difference in purchases or day-7 retention this time. Onboarding only governs the first few minutes; after that, other factors take over. A month confirmed that obvious truth all over again.

What I want to work on next

Next, I plan to fold per-language copy into the config file. Right now Japanese and English live in separate files, but if I tuck the localized strings inside the step definitions, managing six apps times languages gets one notch easier. Alongside that, I want to prepare a few variants of the second screen's copy and test, one app at a time, how far opt-in can move.

If your onboarding has scattered across several apps the way mine had, I hope this gives you a starting point for tidying it up. Thank you for reading.

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

App Dev2026-07-14
Long-Press Context Menus for a Gallery Item in a Rork Expo App
Long-pressing a wallpaper card does nothing, yet iOS users expect a preview and a menu. From why Pressable alone falls short, to a native context menu with zeego, resolving the scroll-vs-long-press conflict, wiring up save and share, and a custom overlay fallback for Android — all with working code.
App Dev2026-07-07
Laying Out Variable-Height Images in Two Columns: A Masonry Wallpaper Gallery in a Rork Expo App
From why numColumns cannot pack variable-aspect images cleanly, to a dependency-free column-balancing algorithm, to keeping virtualization with FlashList masonry and a pragmatic no-dependency fallback, building a wallpaper gallery with real code.
App Dev2026-07-05
Building a One-Time Code Field in Expo — SMS Autofill and Segmented Display Together
A six-digit verification screen looks trivial, but once you account for SMS autofill, pasting, and deleting one digit at a time, it needs real care. Here is how to nail the iOS and Android autofill first, then build a segmented look on top of a single TextInput that does not break.
📚RECOMMENDED BOOKS
Build a Large Language Model (From Scratch)
Sebastian Raschka
LLM Dev
Prompt Engineering for LLMs
Berryman & Ziegler
Prompting
AI Engineering
Chip Huyen
AI Eng
* Contains affiliate links
See all →