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 newAppStore.Platformenum, with valuesiOS / macOS / tvOS / visionOS. Downloads from watchOS are tagged asiOS.
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.sharedreturns aVerificationResult. StoreKit has already checked the JSON web signature, so the developer only needs to unwrap the.verifiedcase.appTransactionIDlets you tie multipleoriginalTransactionIDs for the same user together without a server-to-server call (02:00).originalPlatformis 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.verifiedcase.
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, andissuerIDin App Store Connect under Users & Access → Integrations → In-App Purchases (11:35). - The
transactionIDfield is optional, but you should pass it. It can be theappTransactionIDor the ID of anyTransactionfrom 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:
visibleRelationshiptakesupgrade / downgrade / crossgrade / current / all(17:23).upgradeshows the next tier above the current plan,crossgradepicks the best-value plan in the same tier, andcurrentpairs with an offer for win-back.subscriptionOfferViewDetailActionadds a detailLink button on the view; tapping it routes to your own in-app subscription store.- Pair it with iOS 17’s
subscriptionStatusTaskto get aSubscriptionStatus, translate it into your own model such asSKDemoPlusStatus, 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.
Related sessions
- Automate your development process with the App Store Connect API — App Store Connect adds webhooks for real-time updates on builds, review, and subscription status.
- Optimize your monetization with App Analytics — App Analytics adds subscription and offer dimensions; pair it with this session to measure monetization.
- What’s new in AdAttributionKit — new AdAttributionKit features, covering attribution measurement across ad networks.
- What’s new in App Store Connect — the new web UI and offer code configuration entry point both live here.
Comments
GitHub Issues · utterances