
I just shipped version 1.4 of my radio streaming app, and it was the first release where I had to push a major update through App Review at the same time as a brand-new paid subscription (Monthly / Annual / Lifetime) through IAP Review. I’d read a lot of conflicting advice on how Apple actually handles this in parallel, so I want to share what I observed.
This is not a marketing post. I’ll keep app details minimal at the bottom for context, and the link is there only if anyone wants to look at the actual paywall structure.
The setup
- Solo developer, native SwiftUI app, multi-platform (iOS, iPadOS, macOS, tvOS, watchOS, CarPlay).
- Previous releases were free-only. 1.4 introduced a single “Premium” tier with three SKUs (monthly, annual, lifetime) using StoreKit 2.
- All v1.3 features stay free forever — the paid tier only gates a subset of new 1.4 features (EQ, in-app volume control, sleep timer presets, station alarm, watchOS app, tvOS app).
- Widgets stayed free on purpose. They’re system integrations — paywalling them felt wrong.
- Build submitted with all paid features behind a runtime entitlement check, with a debug toggle for review.
What “parallel review” actually means in practice
When you submit a build that introduces a new IAP, App Review and IAP Review are not actually decoupled in the way the docs imply. Two things happen:
- The build goes into App Review like any other binary.
- Each IAP product in “Ready to Submit” state attaches itself to the next submitted build and gets reviewed alongside it.
If either side is rejected, the whole submission stalls. You don’t get a partial pass where the app ships and the IAP gets reviewed later — not on a first-time IAP submission.
A few things I had to get right before the review queue:
- Screenshots for each IAP, not just the app. Easy to forget when you’re focused on App Store screenshots.
- Review notes that explicitly walk through the paywall flow, including how to trigger it, what’s gated, and — critically — what stays free. I added a one-paragraph “this is the value split” note up top.
- A debug build path to free ↔ premium toggling for the reviewer. I left this in
#if DEBUGand called it out in review notes. This saved at least one rejection cycle. - Sandbox account ready and explicitly mentioned, even though Apple has its own.
Things that almost tripped me up
- Free features moved behind premium = guideline 3.1.2 risk. I’d read horror stories about apps adding paid tiers and getting flagged for taking previously free functionality away from users. I dealt with this by being explicit in review notes and on the App Store listing: “All v1.3 features remain free forever.” No issue — but I think the explicit framing helped.
- Subscription metadata localization. Each subscription needs its display name and description per locale, and they’re reviewed. I support 29 languages in-app, but for the IAP metadata I went with English + a small set of strategic locales for now to keep the surface manageable.
- “Restore Purchases” button. Required, and reviewers do test it. Make sure it works without an active subscription too — it should silently no-op, not show an error.
- StoreKit 2 transaction listener. Has to be running before the app’s main UI appears, otherwise renewed entitlements may not be reflected on cold launch. I put it inside an
init()on the entry point. - Family Sharing flag. You set it per product, and it can’t be changed after the first review without a re-review. Decide deliberately.
What surprised me
- Review time was normal. I expected the IAP layer to slow things down. It didn’t — review came back in roughly the same window as a build-only submission.
- The reviewer hit the paywall. I could see in my analytics (after release) that the review-flagged installs triggered the paywall flow, used the debug toggle, then exited cleanly. So the review notes worked — they actually followed them.
- TestFlight + sandbox is unreliable for cross-device entitlement sync. On watchOS in particular, StoreKit 2 sometimes fails to surface the active subscription until well after install. I ended up adding an iPhone-side fallback: the phone reports premium status to the watch via WatchConnectivity, and the watch trusts that flag if its own StoreKit query hasn’t resolved. Worth knowing if you’re shipping a companion watch app behind a paywall.
- iCloud KVS is the right place for premium-derived state. Not the entitlement itself — StoreKit owns that — but anything the user customizes inside premium features (EQ presets, volume, sleep timer presets, alarms). Means an upgrade on one device immediately makes a user’s existing customizations available on the others.
What I’d do differently next time
- Submit IAPs to review before the build that contains them. You can submit IAP metadata for review independently in App Store Connect; not every team realizes this. It de-risks the build review.
- Cut localization on IAP metadata for the first launch. I tried to do all 29 languages and it was the single biggest source of last-minute work. You can add locales later without re-reviewing the IAP itself.
- Have a clear “hidden” mode for premium UI. I added a
premiumFeaturesHiddentoggle so users who don’t want paid features can hide them entirely, not just see a paywall. This wasn’t required by review, but it cuts down on the “why is this app pushing me to pay” feedback you get from the small fraction of users who really don’t want a paid tier in their face.
Open question for the sub
For anyone who’s done this more than once: do you keep IAP metadata in source control somehow, or accept that App Store Connect is the source of truth? I found myself wishing for a Fastlane-style flow for IAP descriptions across 29 locales, and I’m not sure if I’m missing an existing tool.
Context for anyone curious: the app is Pladio, a multi-platform radio streaming app. Listing here only because someone will ask: https://apps.apple.com/ch/app/pladio-my-radio/id6747711658. Happy to answer specific implementation questions in comments — paywall, StoreKit 2 wiring, watchOS entitlement fallback, whatever’s useful.