WWDC Quick Look 💓 By SwiftGGTeam
What’s new in StoreKit and In-App Purchase

What’s new in StoreKit and In-App Purchase

Watch original video

Highlight

Three parts: core framework upgrades, signature requests, and a new SwiftUI store view.


Core content

Subscription apps keep hitting an awkward wall. A user buys the app on iPad, then downloads it again on Mac, and the developer cannot tell that both installs belong to the same Apple Account. Or a user cancels a subscription and the team wants to win them back, but no one knows whether the user first paid for the app or downloaded it free and subscribed later. Without these facts, monetization strategy is just guesswork.

The WWDC25 StoreKit update follows that thread. The three core types — AppTransaction, Transaction, and RenewalInfo — gain new fields like appTransactionID, originalPlatform, and appAccountToken. Offer codes now cover consumables and non-renewing subscriptions, not just subscriptions. Signature requests move to JWS, and the App Store Server Library shrinks the signing flow to a few lines of code. A new SwiftUI view, SubscriptionOfferView, picks which tier to show based on the user’s current subscription. The goal is simple: give developers accurate identity and purchase state, then hand the marketing decision back to the app.


Details

New fields on AppTransaction and Transaction

AppTransaction describes the original download of the app. Starting in iOS 18.4, two new fields are added (01:42):

  • appTransactionID: a globally unique value for every Apple Account that has downloaded the app, back-deployed to iOS 15. Under Family Sharing, each family member gets their own.
  • originalPlatform: a new AppStore.Platform enum, with values iOS / macOS / tvOS / visionOS. Downloads from watchOS are tagged as iOS.

How to read it:

let result = try await AppTransaction.shared
guard case .verified(let appTransaction) = result else { return }

let downloadID = appTransaction.appTransactionID
let platform = appTransaction.originalPlatform

Key points:

  • AppTransaction.shared returns a VerificationResult. StoreKit has already checked the JSON web signature, so the developer only needs to unwrap the .verified case.
  • appTransactionID lets you tie multiple originalTransactionIDs for the same user together without a server-to-server call (02:00).
  • originalPlatform is a good fit for grandfathering existing users when you switch from a paid app to a free-with-IAP model, so you do not give the new model away for free.

currentEntitlements API upgrade

Starting in iOS 18.4, the old singular Transaction.currentEntitlement(for:) is deprecated. Use the plural Transaction.currentEntitlements(for:) instead:

for await result in Transaction.currentEntitlements(for: "com.app.pro") {
    guard case .verified(let transaction) = result else { continue }
    unlock(productID: transaction.productID)
}

Key points:

  • It returns AsyncSequence<VerificationResult<Transaction>>, so you can iterate through multiple records asynchronously.
  • One product can map to several active transactions. For example, the user has their own subscription and also gets access through Family Sharing (03:58). The old API only returned one and lost information.
  • It still returns VerificationResult, so you still go through the .verified case.

Signature requests and the App Store Server Library

iOS 18.4 adds two purchase options that need a JWS signature: introductoryOfferEligibility and promotionalOffer, back-deployed to iOS 15 (10:29). On the client, you wire them up with a new SwiftUI view modifier:

SubscriptionStoreView(productIDs: ["pro.monthly"])
    .subscriptionPromotionalOffer { product in
        longestFreeTrialOffer(for: product)
    } compactJWS: { product, offer in
        try await NetworkLayer.signPromotionalOffer(
            productID: product.id,
            offerID: offer.id
        )
    }

Key points:

  • The first closure picks which promotional offer to apply to the current product.
  • The second closure returns a compact JWS string, signed by your own server. The client should never hold the signing key.
  • The App Store verifies the JWS before allowing the purchase. That is the proof that the developer authorized this promotion.

On the server, sign with the App Store Server Library (available in Swift, Java, Python, and Node.js):

import AppStoreServerLibrary

let creator = try PromotionOfferV2SignatureCreator(
    bundleID: "com.example.SKDemo",
    signingKey: signingKey,
    keyID: keyID,
    issuerID: issuerID
)

let jws = try creator.createSignature(
    productID: productID,
    offerID: offerID,
    transactionID: appTransactionID
)

Key points:

  • Get bundleID, signingKey, keyID, and issuerID in App Store Connect under Users & Access → Integrations → In-App Purchases (11:35).
  • The transactionID field is optional, but you should pass it. It can be the appTransactionID or the ID of any Transaction from that user (13:31). Including it lets the App Store verify the purchase ownership more precisely.

SubscriptionOfferView and subscription relationships

The new SubscriptionOfferView shows a tier with a single declaration, and picks the right plan based on the user’s current subscription:

SubscriptionOfferView(
    groupID: "21181796",
    visibleRelationship: .upgrade
) {
    Image("PlaceholderIcon")
}
.subscriptionOfferViewDetailAction {
    showFullStore = true
}

Key points:

  • visibleRelationship takes upgrade / downgrade / crossgrade / current / all (17:23). upgrade shows the next tier above the current plan, crossgrade picks the best-value plan in the same tier, and current pairs with an offer for win-back.
  • subscriptionOfferViewDetailAction adds a detailLink button on the view; tapping it routes to your own in-app subscription store.
  • Pair it with iOS 17’s subscriptionStatusTask to get a SubscriptionStatus, translate it into your own model such as SKDemoPlusStatus, and inject it through the environment to downstream views to decide which tier to show (16:08).

Offer code expansion and purchase UI context

Offer codes now cover consumables, non-consumables, and non-renewing subscriptions, back-deployed to iOS 16.3 (07:54). Transaction.Offer.PaymentMode adds a oneTime case, back-deployed to iOS 17.2. On older systems, read it from the offerPaymentModeStringRepresentation string field, back-deployed to iOS 15.

Starting in iOS 18.2, the UIKit and AppKit purchase methods require a UI context. Pass a UIViewController on iOS, macCatalyst, tvOS, and visionOS; pass an NSWindow on macOS; pass nothing on watchOS (09:29). In SwiftUI, read the purchase environment to get a PurchaseAction and call it directly with callAsFunction. StoreKit views handle this for you.


Takeaways

1. Use appTransactionID to rebuild a single user identity

Why it matters: tying a user’s multiple orders together used to require a server-to-server call to the App Store Server API. appTransactionID is available on the client, back-deployed to iOS 15, and works across Family Sharing members.

How to start: call AppTransaction.shared once at app launch and cache appTransactionID as part of the local user identity. After login, bind it to the server account so a single Apple Account is not split into multiple users across devices.

2. Bake originalPlatform into your entitlement logic

Why it matters: when you switch from a paid app to in-app purchase, existing users churn the most. originalPlatform tells you whether the user first bought on iOS, macOS, tvOS, or visionOS, so you can grandfather them in.

How to start: before shipping the new pricing model, record originalPlatform and originalPurchaseDate for each appTransactionID, and keep the old entitlement for users who downloaded before a given cut-off date.

3. Replace hand-written paywalls with SubscriptionOfferView + visibleRelationship

Why it matters: showing upgrade, downgrade, or crossgrade plans based on the current tier is a common need. Doing it by hand means querying SubscriptionStatus, comparing prices within a group, and handling offer eligibility — a lot of code.

How to start: in your App’s subscriptionStatusTask, normalize the status into your own enum (such as none / standard / pro) and pass it down through the environment. Use .current plus subscriptionPromotionalOffer for win-back, and .upgrade for the upgrade slot.

4. Replace hand-written JWS with the App Store Server Library

Why it matters: a wrong signature on a promotional or introductory offer makes the purchase fail outright. Apple’s official library covers Swift, Java, Python, and Node.js, and Apple owns the signing logic.

How to start: pull the App Store Server Library into your signing service and wrap a /sign-promotional-offer route that takes productID, offerID, and transactionID. From the client, the second closure of subscriptionPromotionalOffer issues a GET to that route.


Comments

GitHub Issues · utterances