I'm implementing PurchaseIntent.intents for App Store in-app purchase promotions, following Apple's WWDC guidance. The API only works on cold launch (killed→launch), but fails on background→foreground transitions, making App Store promotions unusable.
Sample code as followed from WWDC23 video "What's new in StoreKit 2 and StoreKit Testing in Xcode".
In the StoreKitManager observable class, I have this function which is initialized in a listening task:
func listenForPurchaseIntent() -> Task<Void, Error> {
return Task { [weak self] in
for await purchase in PurchaseIntent.intents {
guard let self else { continue }
let product = purchase.product
await self.purchaseProduct(product)
}
}
}
where purchaseProduct() will perform the call to:
try await product.purchase()
ISSUE:
When the app is in background (after previously launched), and the purchase intent is initiated from Xcode Transaction Manager or using the "itms-services://?action=purchaseIntent" method, the system foregrounds my app but the purchase intent is never delivered to the waiting listener. The intent remains queued until the next cold launch (quit app and relaunch app). This could mean that if a user has installed the app, and has run the app, then tapped the promotional IAP from the App Store, the purchase intent will not show up until the next cold launch.
If the app is in quit state, then the system will foreground the app, and purchase intent is delivered correctly.
STEPS TO REPRODUCE
Launch app (listener starts in StoreKitManager.init())
Background app
Add purchase intent via Xcode Transaction Manager
Foreground app
Result: No purchase sheet appears, no intent delivered
Workaround attempts:
Using this either in a view or the main app:
func checkForPurchaseIntents() async {
for await purchaseIntent in PurchaseIntent.intents {
await storeKit.purchaseProduct(purchaseIntent.product)
}
}
Applied to .onChange(of: scenePhase) - Doesn't work, nothing happens.
Using UIApplication.willEnterForegroundNotification - Only works on the first time the app goes from background to foreground when purchase intent is sent. Doesn't work on second time or third time.
• Attempting to creating fresh listening task on each foreground - Does not work.
The question is:
How are we supposed to implement the PurchaseIntent API?
I have checked Apple sample projects like BackyardBirds, and sample projects from WWDC on StoreKit 2 but they never implemented Purchase Intent.
StoreKit
RSS for tagSupport in-app purchases and interactions with the App Store using StoreKit.
Selecting any option will automatically load the page
Post
Replies
Boosts
Views
Created
I'm using StoreKit 2 with Product.products(for:) to fetch my auto-renewable subscriptions. It works in the Xcode simulator with a local StoreKit configuration file, but returns an empty array (no error) in TestFlight.
iOS 15+, using async/await API
Products are configured in App Store Connect
Paid Apps agreement is active
Sandbox tester account set up
Has anyone experienced this? What am I missing?
Topic:
App & System Services
SubTopic:
StoreKit
Tags:
Subscriptions
StoreKit Test
StoreKit
TestFlight
Environment
iOS 18.1+
StoreKit External Purchase Link Entitlement (EU)
App distributed via App Store in France
Problem Summary
I'm implementing external purchase links for EU users using ExternalPurchaseCustomLink. While the implementation works correctly in my TestFlight testing, some production users experience token(for:) returning nil.
Implementation
Following Apple's documentation, my flow is:
Check eligibility using ExternalPurchaseCustomLink.isEligible
If eligible, call ExternalPurchaseCustomLink.token(for: "ACQUISITION")
Store the token for use in the external purchase flow
// Simplified implementation
guard #available(iOS 18.1, *) else { return }
let isEligible = await ExternalPurchaseCustomLink.isEligible
guard isEligible else { return }
// This returns nil for some users despite isEligible being true
let token = try await ExternalPurchaseCustomLink.token(for: "ACQUISITION")
Configuration
Entitlement: com.apple.developer.storekit.external-purchase-link is present
Info.plist: SKExternalPurchaseCustomLinkRegions set to ["fr"]
App is only available in France via App Store
Observed Behavior
For affected users:
ExternalPurchaseCustomLink.isEligible returns true
token(for:) returns nil (not throwing an error)
The token generation was never previously called for these users
Questions
Under what conditions does token(for:) return nil when isEligible is true?
Is there additional validation I should perform before calling token(for:)?
Are there known issues with token generation on specific iOS versions?
Any guidance on debugging this issue would be appreciated.
Topic:
App & System Services
SubTopic:
StoreKit
I set up a sandbox account to test in-app purchases in my development app. I went to settings > dev > sandbox accounts and updated my login in the app store. But I received a 'block'; I can no longer download new versions or anything from the store. I've already removed the sandbox account, and even using my real Apple ID, I can't use the functions.
Topic:
App & System Services
SubTopic:
StoreKit
I am a new dev in this space. I need to know how you guys setup a trial period correctly. I am using React Native and RevenueCat SDK. It seems a lot more complicated than it should be. Any assistance is greatly appreciated.
Topic:
App & System Services
SubTopic:
StoreKit
a UK-based user is having trouble completing an in-app purchase.
after going through the typical purchase flow (tapping the button to trigger the in-app purchase sheet, completing Face ID) they see this verification sheet appear over my app and have to go to their banking app to approve the purchase.
after approving the purchase from their banking app, they tap "Payment confirmed on Mobile App" to close the sheet, but then see an alert that suggests the result is .userCancelled.
the purchase does not seem to have completed. the user reports not being charged (despite numerous attempts). plus, i have a "restore purchases" function on App init that would've restored a purchase if it existed.
i have implemented what i think is a typical Storekit.purchase() method (again, the message the user sees is for the .userCancelled case):
func purchase(productId: String) async -> (Bool, String?) {
guard let product = subscriptionProducts.first(where: { $0.id == productId }) else {
return (false, "Product not found")
}
do {
let result = try await product.purchase()
switch result {
case .success(let verification):
switch verification {
case .verified(let transaction):
await transaction.finish()
hasSubscription = true
return (true, nil)
case .unverified:
return (false, "Transaction verification failed")
}
case .userCancelled:
return (false, "No worries, take your time. 😌")
case .pending:
return (false, "Purchase is pending")
u/unknown default:
return (false, "Error purchasing product. If this keeps happening, please contact [email].")
}
} catch {
return (false, "Error purchasing product: \(error.localizedDescription)")
}
}
has anyone dealt with this issue? i was seeing an unusually high number of .userCancelled purchase events from users outside the US, and i'm wondering if some of them were genuine purchase attempts that were blocked by this verification step. 😕
Hey guys,
This is a general question, but I have been working on an app for a while and I would like to to introduce IAP to the app. I have no clue where to start and chat GPT is no help. I also tried to vibe coding the IAP function and that didn't work out well. Any material or advice would help.
Topic:
App & System Services
SubTopic:
StoreKit
We have 2 monthly subscription tiers that are part of a subscription group, and always have been part of this group. Both are configured with a 1 month trial introductory offer. According to the documentation for auto-renewable subscriptions,:
Users can subscribe to one subscription product per group at a time.
And yet several users have managed to start trials of both products in this group simultaneously, which converted to paid subscriptions after the expiration of the trials, and now are being concurrently billed by Apple for both. How do we completely prevent this from happening?
Scenario:
User is actively subscribed to Monthly Package
From the Device App (Manage Subscriptions), user upgrades to Yearly Package
Purchase completes successfully on device
Issue: Do not receive any server notification for this action
Month Package Purchase Date: 2025-11-11 19:06:45.537 +0600 Month to Yearly Upgradation Date: 2025-12-11 paymentReferenceId: 510002270528780
Topic:
App & System Services
SubTopic:
StoreKit
Tags:
App Store Server Notifications
App Store Server API
1)What's the relationship between ""Texas, child, 16-17, significant change declined" from my 11:03 screenshot below and "revoking"? ie what situation could someone decline but not revoke or vice versa, what is the purpose of the distinction in testing or legally or implications/implementation
2) What is a cause besides a typo to receive the "Can't trigger notification" titled message after using "Revoke"? you can see by my screenshot by comparing the left and right half there is no title.
3) Even if i find a workaround perhaps my info will help someone else or be an opportunity for improved documentation
see:
see my 5:54pm second screenshot
Hello,
I would like to ask about an Apple ID authentication behavior during in-app purchases.
Our app uses a StoreKit 1 (SKPaymentQueue-based) implementation, and there are no differences in the in-app purchase logic between the TestFlight build and the App Store production build.
However, we have observed that some users are prompted to enter their Apple ID password during in-app purchases.
The observed behavior is as follows:
On the first in-app purchase, the system prompts for the Apple ID password
After the password is entered once, subsequent purchases proceed normally using Face ID (double side-button press)
Even after deleting and reinstalling the app, or switching between TestFlight and App Store builds, the password prompt does not reappear if authentication has already occurred
This behavior can occur even when the Apple ID already has an active auto-renewable subscription
The only confirmed change on our side is:
The app is now built with Xcode 26 instead of Xcode 18
Based on this, we are currently considering the following possible causes:
A change in purchase authentication behavior due to the Xcode version update
Expiration of the Apple ID purchase authentication session after a long period without purchases
In addition to these points, we would like to ask:
Are there any other common conditions or security policies in iOS or the App Store that may cause the system to require Apple ID password input during in-app purchases?
Is this behavior considered expected under certain circumstances?
We would appreciate your clarification on whether this is expected system behavior or if there are any implementation aspects we should further review.
Thank you for your support.
Hello,
I would like to ask about an Apple ID authentication behavior during in-app purchases.
Our app uses a StoreKit 1 (SKPaymentQueue-based) implementation, and there are no differences in the in-app purchase logic between the TestFlight build and the App Store production build.
However, we have observed that some users are prompted to enter their Apple ID password during in-app purchases.
The observed behavior is as follows:
On the first in-app purchase, the system prompts for the Apple ID password
After the password is entered once, subsequent purchases proceed normally using Face ID (double side-button press)
Even after deleting and reinstalling the app, or switching between TestFlight and App Store builds, the password prompt does not reappear if authentication has already occurred
This behavior can occur even when the Apple ID already has an active auto-renewable subscription
The only confirmed change on our side is:
The app is now built with Xcode 26 instead of Xcode 18
Based on this, we are currently considering the following possible causes:
A change in purchase authentication behavior due to the Xcode version update
Expiration of the Apple ID purchase authentication session after a long period without purchases
In addition to these points, we would like to ask:
Are there any other common conditions or security policies in iOS or the App Store that may cause the system to require Apple ID password input during in-app purchases?
Is this behavior considered expected under certain circumstances?
We would appreciate your clarification on whether this is expected system behavior or if there are any implementation aspects we should further review.
Thank you for your support.
Topic:
App & System Services
SubTopic:
StoreKit
Hello,
I seem to have a strange bug when testing 2 of my Apps that calls SubscriptionStoreView(groupID: storekitmanager.groupID), where storekitmanager is using @Observable, with groupID from the Subscription Group in App Store Connect.
They have the following: Yearly, Biannually, and Monthly. Their productIDs and groupIDs have been configured in each app with the correct IDs.
In my main Apple ID, when the SubscriptionStoreView is presented, selecting Monthly, tap Subscribe, and nothing happens. Selecting either Yearly or Biannually, tap Subscribe and the Confirmation Dialog that triggers faceID/touchID will appear correctly. This happens in both Apps for TestFlight.
I have a Manage Subscriptions button that uses:
manageSubscriptionsSheet(isPresented:subscriptionGroupID:)
I can change the subscription to Monthly in that manage subscriptions.
However, if I switch to a Sandbox Apple Account, the "bug" described above does not happen. The Sandbox account when selecting Monthly and tap Subscribe will trigger the Confirmation Dialog (in both Apps). Not sure if my main account is "stuck" in some loop where it is trying to purchase Monthly in TestFlight but it is not completed.
Has anyone ever encountered such a bug?
In Getting started with In-App Purchase using StoreKit views and the corresponding sample project, Store simultaneously enumerates Transaction.unfinished and Transaction.updates.
Since, "if your app has unfinished transactions, the updates listener receives them once, immediately after the app launches," it appears that Transaction.unfinished would also receive the same unfinished transactions causing handle(updatedTransaction:) to be called for twice for each transaction, causing consumables to be double-counted.
Is this a bug in the sample? Is there more information on concurrent execution of unfinished and updates?
Need to vent a bit before relaxing for Christmas...
WatchOS IAP using Storekit 2 is such a mess...is nobody actually using this or does Apple just not care for the user experience here?
Lots of users experience after the purchase confirmation double tap on the side button an instant return to the purchase screen with nothing actually happening. No error message whatsoever. There is just one remedy: users need to unpair and re-pair their watch, including restoring a backup and setting up their wallet again. Nobody really wants to do this, or doesn't believe me and think this is just typical support BS, because their watch is paired and most things just work as they expect. And it turns away a customer, often leaving a bad review. And I can't do anything about it.
Other errors in the purchase process are reported, but like "process interrupted" in case the payment is not setup correctly (credit car no longer valid or sth.). How should the user know? There must be better ways of letting him know what exactly the problem is.
You need to implement a "Restore Purchase" function, otherwise you're not passing the review. But it really asks every time for the AppStore password, and users with crazy passwords -- that they rightfully should have! -- have almost no chance of typing them successfully on the tiny AW keyboard. Why is it not also just a side button double tap like for purchase? At the very least you would need access to the keychain PWs or allow pasting of sth. copied on the paired iPhone.
Promo Codes for IAP on AW-only apps just don't work. AW has no redemption at all, and on the iPhone the AppStore will try to talk to a companion app (which AW-only doesn't have) and the end up in a dead-end installation effort.
This all feels like never really tested in the field, and people are of course blaming the 3rd party dev. for all these issues. And opening a ticket is just leading nowhere -- at best it's closed after months with the hint "duplicate" but w/o any chance for me to see that one that they then actually work on and track progress.
It's all so frustratingly broken...
Hi everyone.
I'm trying to use https://developer.apple.com/documentation/appstoreserverapi/get-transaction-info to retrieve order information. How can I get the refund status of an order through this API? Also, Apple's webhook notification for refunds includes fields like revocationReason and revocationType. Can these be retrieved through the API? I've noticed that some refund orders have these fields when retrieved using get-transaction-info api, but others don't. I don't know the reason for these differences. Could you please explain?
Thank you very much.
Topic:
App & System Services
SubTopic:
StoreKit
Tags:
App Store Server Notifications
App Store Server API
关于通过Advanced commerce API 创建SKU,有几个问题:
1、通过Advanced commerce API创建的自动续费订阅的SKU是否支持推介优惠、促销优惠?如果可以,是在苹果connect设置吗?
2、已订阅通过connect创建的SKU的用户,是否可以重复订阅通过Advanced commerce API创建的SKU?还是要通过Advanced commerce API变更订阅?
3、Advanced commerce API创建的SKU ,用户订阅成功后,续费的扣款是否仍由苹果发起?还是由开发者自行发起?
Hello,
I’m integrating promotional offers for auto-renewable subscriptions using StoreKit 2.
The offer is displayed correctly, the Apple purchase sheet appears, and I can start the payment flow. The sheet shows the correct discounted price and the end date of the offer. However, after confirming the purchase, an alert appears saying “Unable to Purchase - Contact the developer for more information”
When dismissing the alert, Xcode logs the following:
Purchase did not return a transaction:
Error Domain=ASDServerErrorDomain Code=3902
"No se ha podido realizar la compra"
UserInfo={
NSLocalizedFailureReason=No se ha podido realizar la compra,
client-environment-type=Sandbox,
AMSServerErrorCode=3902,
storefront-country-code=ESP
}
Test environment:
App installed from Xcode on a real iPhone
Logged in with a Sandbox Apple ID
Using StoreKit 2
Promotional offer applied using:
Product.PurchaseOption.promotionalOffer(_:compactJWS:)
On the server side, I generate the promotional offer signature exactly as described in Apple’s documentation:
https://developer.apple.com/documentation/storekit/generating-a-signature-for-promotional-offers
The signature is generated using a Subscription Key
Signed with ECDSA + SHA256
Uses the correct invisible separator (U+2063)
The signature is validated locally using the derived public key and verifies correctly
The sandbox user has had previous subscriptions, which is why this promotional offer is eligible and shown.
Given that:
The offer is displayed correctly
The purchase sheet shows the discounted price and duration
The signature validates locally
The error occurs only after confirming the purchase
My question is:
Is this a known limitation or issue with promotional offers in the Sandbox environment?
Should promotional offers be tested exclusively via TestFlight instead of Sandbox?
Any clarification would be greatly appreciated.
Thank you!
I’m implementing a subscription purchase flow using promo code redemption via an external App Store URL.
Flow:
User taps “Purchase” in the app (spinner shown)
App opens the promo redemption URL (apps.apple.com/redeem)
User completes redemption in the App Store
User returns to the app
The app must determine whether the subscription was purchased within a reasonable time window
The app listens to Transaction.updates and also checks
Transaction.currentEntitlements when the app returns to the foreground.
Issue:
After redeeming a subscription promo code via the App Store and returning to the
app, the app cannot reliably determine whether the subscription was successfully
purchased within a short, user-acceptable time window.
In many cases, neither Transaction.updates nor
Transaction.currentEntitlements reflects the newly redeemed subscription
immediately after returning to the app. The entitlement may appear only after a
significant delay, or not within a 60-second timeout at all, even though the
promo code redemption succeeded.
Expected:
When the user returns to the app after completing promo code redemption,
StoreKit 2 should report the updated subscription entitlement shortly thereafter
(e.g. within a few seconds) via either Transaction.updates or
Transaction.currentEntitlements.
Below is the minimal interactor used in the sample project. The app considers
the purchase successful if either a verified transaction for the product is received via Transaction.updates, or the product appears in Transaction.currentEntitlements when the app returns to the foreground. Otherwise, the flow fails after a 60-second timeout.
Questions:
Is this entitlement propagation delay expected when redeeming promo codes through the App Store?
Is there a recommended API or flow for immediately determining whether a subscription has been successfully redeemed?
Is there a more reliable way to detect entitlement changes after promo code redemption without triggering user authentication prompts (e.g., from AppStore.sync())?
import UIKit
import StoreKit
final class PromoPurchaseInteractor {
private let timeout: TimeInterval = 60
private struct PendingOfferRedemption {
let productId: String
let completion: (Result<Bool, Error>) -> Void
}
private var pendingRedemption: PendingOfferRedemption?
private var updatesTask: Task<Void, Never>?
private var timeoutTask: Task<Void, Never>?
enum DefaultError: Error {
case generic
case timeout
}
init() {
NotificationCenter.default.addObserver(self, selector: #selector(willEnterForeground), name: UIApplication.willEnterForegroundNotification, object: nil)
}
deinit {
NotificationCenter.default.removeObserver(self)
updatesTask?.cancel()
timeoutTask?.cancel()
}
func purchaseProduct(using offerUrl: URL, productId: String, completion: @escaping (Result<Bool, Error>) -> Void) {
guard pendingRedemption == nil else {
completion(.failure(DefaultError.generic))
return
}
pendingRedemption = PendingOfferRedemption(productId: productId, completion: completion)
startPurchase(using: offerUrl)
}
@objc private func willEnterForeground() {
guard let pendingRedemption = pendingRedemption else { return }
startTimeoutObserver()
Task {
if await hasEntitlement(for: pendingRedemption.productId) {
await MainActor.run {
self.completePurchase(result: .success(true))
}
}
}
}
private func startPurchase(using offerURL: URL) {
startTransactionUpdatesObserver()
UIApplication.shared.open(offerURL) { [weak self] success in
guard let self = self else { return }
if !success {
self.completePurchase(result: .failure(DefaultError.generic))
}
}
}
private func completePurchase(result: Result<Bool, Error>) {
stopTransactionUpdatesObserver()
stopTimeoutObserver()
guard let _ = pendingRedemption else { return }
pendingRedemption?.completion(result)
pendingRedemption = nil
}
private func startTransactionUpdatesObserver() {
updatesTask?.cancel()
updatesTask = Task {
for await update in Transaction.updates {
guard case .verified(let transaction) = update else { continue }
await MainActor.run { [weak self] in
guard let self = self,
let pending = self.pendingRedemption,
transaction.productID == pending.productId
else { return }
self.completePurchase(result: .success(true))
}
await transaction.finish()
}
}
}
private func stopTransactionUpdatesObserver() {
updatesTask?.cancel()
updatesTask = nil
}
private func startTimeoutObserver() {
guard pendingRedemption != nil else { return }
timeoutTask?.cancel()
timeoutTask = Task {
try? await Task.sleep(nanoseconds: UInt64(timeout * 1_000_000_000))
await MainActor.run { [weak self] in
self?.completePurchase(result: .failure(DefaultError.timeout))
}
}
}
private func stopTimeoutObserver() {
timeoutTask?.cancel()
timeoutTask = nil
}
private func hasEntitlement(for productId: String) async -> Bool {
for await result in Transaction.currentEntitlements {
guard case .verified(let transaction) = result else { continue }
if transaction.productID == productId {
return true
}
}
return false
}
}
is there a way to make a test subscription in-app purchase expire immediately, for faster testing? it seems exceedingly complicated to test subscriptions if we have to a) wait until the next day for expiry, or b) keep on creating new apple ids to get into a fully unsubscribed state? it is still kind of madness testing this stuff, after all the years it has been available.