◉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)●SDK 58 — Beta since 09/15 with no stable date yet; running projects in Expo Go has required login since 09/03●WIDGET — A write-up traces an iOS widget that renders black only in production builds●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)●SDK 58 — Beta since 09/15 with no stable date yet; running projects in Expo Go has required login since 09/03●WIDGET — A write-up traces an iOS widget that renders black only in production builds●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/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 Purchase7Rork Max236Monetization39UX5

✦ 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-05-05
Rork Max × RevenueCat Paywalls SDK: Remote Paywalls, A/B Testing, and Conversion Gains
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.
⟐ Business2026-04-26
Taking a Rork Max Native App Up the App Store Charts — The Order I Build Ads, StoreKit 2, and Subscriptions
The order I follow to grow a Rork Max SwiftUI app into one that stays near the top of its App Store category: a single AdGate that decides when ads may show, StoreKit 2 Transaction.updates monitoring, and where I draw the lines on ASO and retention — with working code.