Irbisa · Cheatsheet
StoreKit & App Store Release
StoreKit 2, subscriptions and offers, entitlements, server notifications, refunds, testing purchases, App Review, phased release, forced update
Which purchases must go through Apple's in-app purchase, and which can use Apple Pay or your own checkout?
What are the four in-app purchase types, and what does each one oblige the app to support?
Walk through a purchase with StoreKit 2: which calls happen between loading the product and unlocking the content?
import StoreKit @MainActorfinal class PaywallModel { private(set) var products: [Product] = [] var unlock: (String) -> Void = { _ in }…What is the difference between
CFBundleShortVersionStringandCFBundleVersion, and which one must change for every upload?How do you ask for an App Store rating with
requestReview, and why can the app not tell whether the dialog appeared?import StoreKitimport SwiftUI struct OrderPlacedView: View { @Environment(\.requestReview) private var requestReview let completedOrders: Int…Why must the app start listening to
Transaction.updatesat launch, and which purchases are lost without it?import StoreKit @MainActorfinal class Store { static let shared = Store() private(set) var ownedProductIDs: Set<String> = []…Find the bug: when the backend is down during a purchase, the user is charged and the coins never arrive, not even after the backend recovers.
func handle(_ result: VerificationResult<Transaction>) async { guard case .verified(let transaction) = result else { return } await transaction.finish() do { // The backend verifies the JWS and adds the coins to the account try await api.creditCoins(signedTransaction: result.jwsRepresentation) await wallet.reload() } catch { logger.error("credit failed: \(error)") showToast("Something went wrong. Please try again.") }}// Called for purchase() results and for every Transaction.updates elementHow do you decide what the user owns with
Transaction.currentEntitlements, and why is anisPremiumflag inUserDefaultsthe wrong source of truth?import StoreKit enum Tier: Comparable { case free, plus, pro } @MainActorfinal class Entitlements {…What does Restore Purchases do in StoreKit 2, why does App Review still expect the button, and when should you call
AppStore.sync()?import StoreKitimport SwiftUI struct RestoreButton: View { let refresh: () async -> Bool // recomputes entitlements; true if anything is owned @State private var note: String?…How do subscription groups and levels work, and what happens on an upgrade, a downgrade and a crossgrade?
How do you show a free trial only to users eligible for it, and how do introductory offers, promotional offers and offer codes differ?
import StoreKit // The line under a plan on the paywallfunc offerLine(for product: Product) async -> String { guard let subscription = product.subscription, let intro = subscription.introductoryOffer,…A subscriber's card fails at renewal. What are billing retry and the billing grace period, and what should the app show in each state?
import StoreKit enum BillingBanner { case none case fixPayment(accessUntil: Date?) // grace period: still premium case paymentFailed // billing retry: access is off, Apple keeps retrying…A user gets a refund from Apple. How do the app and your server find out, and what does
beginRefundRequestlet you offer in the app?import StoreKitimport SwiftUI struct PurchaseRow: View { let transactionID: StoreKit.Transaction.ID // SwiftUI has its own Transaction type @State private var showRefund = false…How do you test in-app purchases with a StoreKit configuration file, the sandbox and TestFlight, and what does each one not catch?
import StoreKitimport StoreKitTestimport XCTest final class EntitlementTests: XCTestCase { func testRefundRemovesTheEntitlement() async throws {…App Review rejects your build. How do you respond in App Store Connect, when do you appeal, and when do you ask for an expedited review?
The App Store has no forced update. How do you move users to a minimum supported version, and what must still work in the old build?
import Foundation // Served by your backend or remote config, cached on disk, fetched at launchstruct VersionPolicy: Decodable { let minimum: String // below it: a blocking screen let recommended: String // below it: a banner the user can dismiss…How does your backend learn about renewals, refunds and cancellations, and why trust Apple's signed JWS transaction rather than the app's word?
{ "notificationType": "DID_FAIL_TO_RENEW", "subtype": "GRACE_PERIOD", "notificationUUID": "002e14d5-51f5-4503-b5a8-c3a1af68eb20", "version": "2.0", "signedDate": 1761292800000,…A user subscribes on iPhone, then signs in on Android and the web. Where does the entitlement live, and how does
appAccountTokentie a purchase to your account?import StoreKit struct Account { let id: String; let storeToken: UUID } // storeToken is created by your server func buy(_ product: Product, for account: Account) async throws -> Product.PurchaseResult { // The UUID comes back in the signed transaction and in every server notification…A release crashes on launch for a third of users and the App Store has no rollback. What can you do in the first hour, and what should have existed before?