Irbisa · Cheatsheet

StoreKit & App Store Release

StoreKit 2, subscriptions and offers, entitlements, server notifications, refunds, testing purchases, App Review, phased release, forced update

Middle Developer · 19 questions

  1. 01

    Which purchases must go through Apple's in-app purchase, and which can use Apple Pay or your own checkout?

    Anything digital that is used inside the app (premium features, a content subscription, coins, ad removal) must be sold through in-app purchase.

  2. 02

    What are the four in-app purchase types, and what does each one oblige the app to support?

    Consumables (coins, credits) are bought again and again, non-consumables (a lifetime unlock) once and forever, auto-renewable subscriptions renew until cancelled, and non-renewing subscriptions (a season pass) simply end.

  3. 03

    Walk through a purchase with StoreKit 2: which calls happen between loading the product and unlocking the content?

    Load the products with Product.products(for:), call purchase() on the one the user taps, check that the result is .success(.verified(transaction)), deliver the content, then call transaction.finish().

    import StoreKit @MainActorfinal class PaywallModel {  private(set) var products: [Product] = []  var unlock: (String) -> Void = { _ in }…
  4. 04

    What is the difference between CFBundleShortVersionString and CFBundleVersion, and which one must change for every upload?

    CFBundleShortVersionString is the version users see in the App Store (2.4.0); CFBundleVersion is the build number (1873).

  5. 05

    How do you ask for an App Store rating with requestReview, and why can the app not tell whether the dialog appeared?

    Call the requestReview action from the SwiftUI environment (in UIKit, AppStore.requestReview(in:)) at a happy moment, and the system decides whether to show the dialog.

    import StoreKitimport SwiftUI struct OrderPlacedView: View {  @Environment(\.requestReview) private var requestReview  let completedOrders: Int…
  6. 06

    Why must the app start listening to Transaction.updates at launch, and which purchases are lost without it?

    Many transactions never pass through your purchase() call: renewals, Ask to Buy approvals, purchases on another device, refunds, offer codes redeemed in the App Store, and anything a crash left unfinished.

    import StoreKit @MainActorfinal class Store {  static let shared = Store()  private(set) var ownedProductIDs: Set<String> = []…
  7. 07

    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 element

    finish() is called before the coins are credited.

  8. 08

    How do you decide what the user owns with Transaction.currentEntitlements, and why is an isPremium flag in UserDefaults the wrong source of truth?

    Recompute ownership from Transaction.currentEntitlements at launch and after every update: it holds the latest signed transaction for each active subscription and non-consumable.

    import StoreKit enum Tier: Comparable { case free, plus, pro } @MainActorfinal class Entitlements {…
  9. 09

    What does Restore Purchases do in StoreKit 2, why does App Review still expect the button, and when should you call AppStore.sync()?

    In StoreKit 2 restoring is mostly automatic: transactions sync to every device on the same Apple Account and show up in currentEntitlements.

    import StoreKitimport SwiftUI struct RestoreButton: View {  let refresh: () async -> Bool   // recomputes entitlements; true if anything is owned  @State private var note: String?…
  10. 10

    How do subscription groups and levels work, and what happens on an upgrade, a downgrade and a crossgrade?

    A subscription group holds plans that replace each other; a user has at most one active plan per group.

  11. 11

    How do you show a free trial only to users eligible for it, and how do introductory offers, promotional offers and offer codes differ?

    Show the trial only when isEligibleForIntroOffer says yes: one introductory offer per subscription group per Apple Account.

    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,…
  12. 12

    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?

    When a renewal fails, Apple retries the charge for up to 60 days (billing retry).

    import StoreKit enum BillingBanner {  case none  case fixPayment(accessUntil: Date?)   // grace period: still premium  case paymentFailed                    // billing retry: access is off, Apple keeps retrying…
  13. 13

    A user gets a refund from Apple. How do the app and your server find out, and what does beginRefundRequest let you offer in the app?

    Apple decides refunds, not you.

    import StoreKitimport SwiftUI struct PurchaseRow: View {  let transactionID: StoreKit.Transaction.ID   // SwiftUI has its own Transaction type  @State private var showRefund = false…
  14. 14

    How do you test in-app purchases with a StoreKit configuration file, the sandbox and TestFlight, and what does each one not catch?

    Use three layers: a .storekit configuration file for fast local runs and unit tests, the sandbox for the real App Store Connect products and server notifications, and TestFlight for the release build in testers' hands.

    import StoreKitimport StoreKitTestimport XCTest final class EntitlementTests: XCTestCase {  func testRefundRemovesTheEntitlement() async throws {…
  15. 15

    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?

    Read the cited guideline and the reviewer's notes, then fix and resubmit, or reply in App Store Connect to explain or ask what they need.

  16. 16

    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?

    Ship the check before you need it: at launch the app fetches a minimum and a recommended version from your server, compares them with its own CFBundleShortVersionString, and shows a blocking screen or a banner that opens the App Store page.

    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…
  17. 17

    How does your backend learn about renewals, refunds and cancellations, and why trust Apple's signed JWS transaction rather than the app's word?

    Apple pushes App Store Server Notifications V2 to an HTTPS endpoint you register, and the App Store Server API answers questions on demand.

    {  "notificationType": "DID_FAIL_TO_RENEW",  "subtype": "GRACE_PERIOD",  "notificationUUID": "002e14d5-51f5-4503-b5a8-c3a1af68eb20",  "version": "2.0",  "signedDate": 1761292800000,…
  18. 18

    A user subscribes on iPhone, then signs in on Android and the web. Where does the entitlement live, and how does appAccountToken tie a purchase to your account?

    On your server, keyed by your user ID: each store knows only its own purchases.

    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…
  19. 19

    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?

    Stop the spread, then roll forward: pause the phased release, switch the crashing path off from the server if you can, and ship a fix (or the previous version's code under a higher version number) with an expedited review request.