RORK LABJP
MAX — Rork Max is built on Claude Code and Claude Opus 4.6, generating native Swift apps directly instead of React NativeAPPLE — Rork Max targets the whole Apple ecosystem: iPhone, iPad, Apple Watch, Apple TV, Vision Pro, and iMessageWORKFLOW — In practice, users settle into letting the AI scaffold while they rewrite the state management and data layer themselvesSEED — Rork raised a $15M seed led by Left Lane Capital in April, with Peak XV, True Ventures, and a16z Speedrun joiningPAPERLINE — Rork acquired app builder Paperline and says it will stay acquisitive to bring in engineering talentREVIEW — Three-month revisit reviews are growing, clarifying where the tool shines and where it doesn'tMAX — Rork Max is built on Claude Code and Claude Opus 4.6, generating native Swift apps directly instead of React NativeAPPLE — Rork Max targets the whole Apple ecosystem: iPhone, iPad, Apple Watch, Apple TV, Vision Pro, and iMessageWORKFLOW — In practice, users settle into letting the AI scaffold while they rewrite the state management and data layer themselvesSEED — Rork raised a $15M seed led by Left Lane Capital in April, with Peak XV, True Ventures, and a16z Speedrun joiningPAPERLINE — Rork acquired app builder Paperline and says it will stay acquisitive to bring in engineering talentREVIEW — Three-month revisit reviews are growing, clarifying where the tool shines and where it doesn't
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 Max223Monetization37UX5

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-07-02
Protecting Ad eCPM in Your Rork Max App: Designing ATT Pre-Permission Priming
For iOS apps built with Rork Max, ad revenue swings heavily on your ATT opt-in rate. Here is how to design a pre-permission priming screen, implement it in SwiftUI, measure the opt-in rate, and order AdMob init correctly.
Business2026-07-01
Before You Reach for Tap to Pay on iPhone in a Rork Max App — Eligibility, Choosing a Payment Provider, and a Realistic Design
Implementation notes for judging, from a business angle, whether you can put Tap to Pay — accepting in-person payments on an iPhone alone — into a Rork Max app. Covers the organization-account and entitlement requirements, why you need a payment provider (PSP), the split between ProximityReader and the PSP, and a realistic path for an indie developer, told candidly.
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.
📚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 →