RORK LABJP
FUNDING — Rork raises a $15M seed led by Left Lane CapitalRORK MAX — Rork Max generates native Swift apps instead of React NativePLATFORM — It targets iPhone, iPad, Watch, and Vision Pro, reaching Live Activities and Core MLGROWTH — Traffic keeps climbing at 743K monthly visits and 85% growthTEST — The Companion app lets you test on a real device without a paid Apple Developer accountSTACK — Built on React Native and Expo for true native experiences, not web wrappersFUNDING — Rork raises a $15M seed led by Left Lane CapitalRORK MAX — Rork Max generates native Swift apps instead of React NativePLATFORM — It targets iPhone, iPad, Watch, and Vision Pro, reaching Live Activities and Core MLGROWTH — Traffic keeps climbing at 743K monthly visits and 85% growthTEST — The Companion app lets you test on a real device without a paid Apple Developer accountSTACK — Built on React Native and Expo for true native experiences, not web wrappers
Articles/Business
Business/2026-06-30Intermediate

Holding App Store Messages Until the Right Moment

Price-increase consent, billing-issue, and win-back messages from the App Store appear right at launch by default. Here is how to take control of StoreKit 2 Messages and defer them until a moment that does not interrupt onboarding or checkout, with working Swift.

StoreKit8In-App Purchase6Rork Max195Monetization33UX5

Premium Article

A price-increase consent sheet once slid up on the third onboarding screen for a brand-new user. As an indie developer running a few apps with in-app purchases on the App Store, I see onboarding completion drop noticeably whenever that interruption fires. We are asking someone to agree to a price before they have felt any value — so the drop makes sense.

App Store system messages (price-increase consent, billing issues, win-back offers) are displayed by StoreKit automatically when your app returns to the foreground. You cannot avoid showing them, but you fully control when. By subscribing to StoreKit 2's Message, you move the trigger into your own hands. This assumes the native Swift that Rork Max generates, but the same approach applies if you bridge something like react-native-iap.

The default behavior, and the one lever you hold

The key fact is that messages can be held. When you subscribe to the StoreKit.Message.messages async sequence, any message the system wants to present flows to your app instead. Once you are inside that for await loop, nothing displays until you call message.display(in:) yourself.

So "subscribing" effectively means "suppress auto-presentation and take over." A received message is retained until you display it; if you never do, it arrives again next launch. You will not lose messages, but never showing them hurts both compliance and revenue, so always design a path that eventually flushes everything.

Open exactly one subscription at launch

Keep the subscription to a single loop for the whole app. Running for await in several places risks processing the same message twice. Create one @MainActor coordinator and start the loop right at launch.

import StoreKit
import SwiftUI
 
@MainActor
final class StoreMessageCoordinator: ObservableObject {
    private(set) var pending: [Message] = []
    /// Whether it is OK to present right now (driven by screen state)
    var canPresent: Bool = false { didSet { flushIfPossible() } }
 
    private var listenTask: Task<Void, Never>?
 
    func start() {
        guard listenTask == nil else { return }
        listenTask = Task { [weak self] in
            for await message in Message.messages {
                // The system will not auto-present unless we call display()
                self?.enqueue(message)
            }
        }
    }
 
    private func enqueue(_ message: Message) {
        // Consent and billing tie directly to revenue/compliance — keep them at the front
        switch message.reason {
        case .priceIncreaseConsent, .billingIssue:
            pending.insert(message, at: 0)
        default:
            pending.append(message)
        }
        flushIfPossible()
    }
 
    private func flushIfPossible() {
        guard canPresent, let scene = Self.activeScene() else { return }
        let next = pending
        pending.removeAll()
        for message in next {
            do { try message.display(in: scene) }
            catch { pending.append(message) } // put it back if we cannot present
        }
    }
 
    private static func activeScene() -> UIWindowScene? {
        UIApplication.shared.connectedScenes
            .compactMap { $0 as? UIWindowScene }
            .first { $0.activationState == .foregroundActive }
    }
}

Flip canPresent based on screen state and you control timing completely. Note that display(in:) requires a UIWindowScene; calling it before a scene is frontmost fails, so only present when a foregroundActive scene is available.

Thank you for reading this far.

Continue Reading

What follows includes implementation code, benchmarks, and practical content we hope you'll find useful. This site runs without ads — server and development costs are supported entirely by members like you. If it's been helpful, we'd be truly grateful for your support.

WHAT YOU'LL LEARN
Subscribe to StoreKit 2 Message.messages and choose exactly when to call message.display(in:) — with working Swift
A per-reason policy table for priceIncreaseConsent / billingIssue / generic deciding show-now vs. defer
A pending queue that holds messages during onboarding, checkout, and full-screen playback, then flushes on return to home
Secure payment via Stripe · Cancel anytime

Unlock This Article

Get full access to the rest of this article. Buy once, read anytime. This site is ad-free — your support goes directly toward keeping it running.

or
Unlock all articles with Membership →
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 →

Related Articles

Business2026-06-14
Will Rork Max's $200 a Month Pay for Itself? Decide It With a Formula
When you are torn over committing to Rork Max at $200 a month, here is a break-even formula and a tiny copy-paste script to decide by the numbers instead of by feel, with notes from indie development.
Business2026-05-05
Rork Max × RevenueCat Paywalls SDK: Remote Paywalls, A/B Testing & Conversion Optimization Complete Guide
A complete guide to integrating RevenueCat Paywalls SDK with Rork Max apps — enabling remote paywall updates and A/B testing without App Store reviews, with production-ready code examples and conversion optimization strategies.
Business2026-05-03
Revenue Flow Design for Rork Max-Released Apps — Offer Codes, Win-Back, and Push Notification Integration
A production-ready implementation walkthrough for maximizing recurring revenue on Rork Max apps. Covers offer code distribution, win-back offers, and push notification integration on both StoreKit 2 and Google Play Billing.
📚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 →