Product.SubscriptionInfo.isEligibleForIntroOffer(for: "21340582")
In the production environment, I have already used the intro offer for this group, but this method still returns true.
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
Activity
Hi Apple Developer Team,
I'm looking to confirm some technical details regarding the pre-order flow and App Store receipt handling. Specifically, I have the following questions:
Q1: After a user installs an app via pre-order and launches it for the first time, will a valid App Store receipt be available immediately via [[NSBundle mainBundle] appStoreReceiptURL]? Are there any known cases where the receipt might be missing or invalid, requiring a manual refresh (e.g., via SKReceiptRefreshRequest)?
Q2: Is the pre-order flow currently supported in the sandbox environment? Specifically, is it possible to simulate pre-ordering an app and installing it in a sandbox or TestFlight environment, in order to test receipt generation and related logic?
https://developer.apple.com/documentation/appstorereceipts/responsebody/receipt
Q3: The receipt field in the App Store receipt structure is marked as deprecated. Is it still acceptable to use this field for validating receipts? Has Apple announced any timeline or system version in which this field will be fully removed or unsupported?
For testing purposes, I log out of media and purchases to use sandbox ID's to test in app purchases. Once I log out of media and purchases in device settings, in app purchases is still using my real apple ID, I'm not being prompted anymore to log into a sandbox ID for purchases.
Topic:
App & System Services
SubTopic:
StoreKit
Hi, I've migrated my app to another developer account more than half a year ago, but I'm still receiving a few transaction payments in the old developer account, which currently has no app.
The payment date shown in the report is last month.
I'm wondering how could this happen. Is it possible for users to initiate a transaction half a year ago and only successfully pay it now?
Hi
To test SkAdNetwork, I installed the profile from the documentation (https://developer.apple.com/documentation/storekit/testing-ad-attributions-with-a-downloaded-profile) on my ios device.
and turned off and on Allow Apps to Request to Track to change the idfa value.
In this state, I installed the app (bundle id: id436731843) using SKAdNetwork with the following values at April 8th AM 01 (UTC)
SkAdnetwork version: 3.0
fidelity
fidelity=0, nonce=b3346a51-f7b5-42a2-a508-775a62317c83, timestamp=1744076349671, signature=...
fidelity=1, nonce=b3346a51-f7b5-42a2-a508-775a62317c83, timestamp=1744076349672, signature=...
itunesitem=436731843
network=87u5trcl3r.skadnetwork
sourceapp=1220307907
I was hoping to get a postback after , but after a day, the postback is still not delivered.
Any idea why the postback is not delivered?
Thank you
Topic:
App & System Services
SubTopic:
StoreKit
iOS Storekit2 Appstore production environment, some user feedback in app purchase faliure, What our log records is StoreKitError.unknown,please How to solve problem, thanks
I cannot test IAP using sandbox account that I logged on from settings, it keeps show my regular Apple ID when I try to test IAP from TestFlight
This issue frustrate me and bunch of my colleagues, we cannot manage our subscription when sandbox account is default choosing Apple ID
I've seen people complaining about this issue a lot
Any help on resolving this issue would be really appreaciated
My server is able to receive notifications for successful purchases. However, we are experiencing an issue where we do not receive any server notifications when a consumable product is refunded.
Could you please help us verify if this behavior is expected? Also, is there a way to trigger a test refund notification for consumable products in the sandbox environment, so we can ensure our server is correctly set up to handle it?
Topic:
App & System Services
SubTopic:
StoreKit
Hi,
I have developed an app which has two in-app purchase subscriptions. During the test, the app can successfully get the status of the subscriptions. After it's released, I downloaded it from app store and subscribed it with my apple account. I found that in most cases, the app can identify that I have subscribed it and I can use its all functions. But yesterday, when I launched it again, it showed the warning that I haven't subscribed it. I checked my subscription in my account and the subscription status hasn't been changed, that is, I have subscribed it. And after one hour, I launched it again. This time the app identified that I have subscribed it. Why? The following is the code about listening to the subscription status. Is there any wrong about it?
HomeView()
.onAppear(){
Task {
await getSubscriptionStatus()
}
}
func getSubscriptionStatus() async {
var storeProducts = [Product]()
do {
let productIds = ["6740017137","6740017138"]
storeProducts = try await Product.products(for: productIds)
} catch {
print("Failed product request: \(error)")
}
guard let subscription1 = storeProducts.first?.subscription else {
// Not a subscription
return
}
do {
let statuses = try await subscription1.status
for status in statuses {
let info = try checkVerified(status.renewalInfo)
switch status.state {
case .subscribed:
if info.willAutoRenew {
purchaseStatus1 = true
debugPrint("getSubscriptionStatus user subscription is active.")
} else {
purchaseStatus1 = false
debugPrint("getSubscriptionStatus user subscription is expiring.")
}
case .inBillingRetryPeriod:
debugPrint("getSubscriptionStatus user subscription is in billing retry period.")
purchaseStatus1 = false
case .inGracePeriod:
debugPrint("getSubscriptionStatus user subscription is in grace period.")
purchaseStatus1 = false
case .expired:
debugPrint("getSubscriptionStatus user subscription is expired.")
purchaseStatus1 = false
case .revoked:
debugPrint("getSubscriptionStatus user subscription was revoked.")
purchaseStatus1 = false
default:
fatalError("getSubscriptionStatus WARNING STATE NOT CONSIDERED.")
}
}
} catch {
// do nothing
}
guard let subscription2 = storeProducts.last?.subscription else {
// Not a subscription
return
}
do {
let statuses = try await subscription2.status
for status in statuses {
let info = try checkVerified(status.renewalInfo)
switch status.state {
case .subscribed:
if info.willAutoRenew {
purchaseStatus2 = true
debugPrint("getSubscriptionStatus user subscription is active.")
} else {
purchaseStatus2 = false
debugPrint("getSubscriptionStatus user subscription is expiring.")
}
case .inBillingRetryPeriod:
debugPrint("getSubscriptionStatus user subscription is in billing retry period.")
purchaseStatus2 = false
case .inGracePeriod:
debugPrint("getSubscriptionStatus user subscription is in grace period.")
purchaseStatus2 = false
case .expired:
debugPrint("getSubscriptionStatus user subscription is expired.")
purchaseStatus2 = false
case .revoked:
debugPrint("getSubscriptionStatus user subscription was revoked.")
purchaseStatus2 = false
default:
fatalError("getSubscriptionStatus WARNING STATE NOT CONSIDERED.")
}
}
} catch {
// do nothing
}
if purchaseStatus1 == true || purchaseStatus2 == true {
purchaseStatus = true
} else if purchaseStatus1 == false && purchaseStatus2 == false {
purchaseStatus = false
}
return
}
Topic:
App & System Services
SubTopic:
StoreKit
Issue Description
When using the App Store Server API endpoint GET v2/history/{transactionId} to retrieve transaction history for a specific transaction, I'm observing unexpected changes in the appTransactionId field across related transactions in the same subscription group.
Important Context: This is a "clean" auto-renewable subscription with no user intervention - the user has had continuous auto-renewals without any upgrades, downgrades, cancellations, or resubscriptions. The subscription has been renewing automatically and successfully throughout the entire period.
API Call
GET v2/history/1000000000000001
Response Data
The API returns the following transaction history, where I notice the appTransactionId values are inconsistent across what should be a straightforward auto-renewal sequence:
Note: The data below has been sanitized for privacy protection (IDs, bundle identifiers, etc. have been replaced with example values), but the logical relationships, date sequences, and the core issue remain identical to the original data.
Array
(
[0] => Array
(
[transactionId] => 1000000000000001
[originalTransactionId] => 1000000000000001
[webOrderLineItemId] => 1000000000000001
[bundleId] => com.example.myapp
[productId] => MonthlySubscription
[subscriptionGroupIdentifier] => 20000000
[purchaseDate] => 1743784032000
[originalPurchaseDate] => 1743784034000
[expiresDate] => 1746376032000
[quantity] => 1
[type] => Auto-Renewable Subscription
[inAppOwnershipType] => PURCHASED
[signedDate] => 1751868174651
[environment] => Production
[transactionReason] => PURCHASE // Original purchase
[storefront] => USA
[storefrontId] => 143441
[price] => 100000
[currency] => USD
[appTransactionId] => 700000000000000001 // Different value
)
[1] => Array
(
[transactionId] => 1000000000000002
[originalTransactionId] => 1000000000000001
[webOrderLineItemId] => 1000000000000002
[bundleId] => com.example.myapp
[productId] => MonthlySubscription
[subscriptionGroupIdentifier] => 20000000
[purchaseDate] => 1746376032000
[originalPurchaseDate] => 1746347349000
[expiresDate] => 1749054432000
[quantity] => 1
[type] => Auto-Renewable Subscription
[inAppOwnershipType] => PURCHASED
[signedDate] => 1751868174651
[environment] => Production
[transactionReason] => RENEWAL // First auto-renewal
[storefront] => USA
[storefrontId] => 143441
[price] => 100000
[currency] => USD
[appTransactionId] => 700000000000000002 // Same for renewals
)
[2] => Array
(
[transactionId] => 1000000000000003
[originalTransactionId] => 1000000000000001
[webOrderLineItemId] => 1000000000000003
[bundleId] => com.example.myapp
[productId] => MonthlySubscription
[subscriptionGroupIdentifier] => 20000000
[purchaseDate] => 1749054432000
[originalPurchaseDate] => 1749025657000
[expiresDate] => 1751646432000
[quantity] => 1
[type] => Auto-Renewable Subscription
[inAppOwnershipType] => PURCHASED
[signedDate] => 1751868174651
[environment] => Production
[transactionReason] => RENEWAL // Second auto-renewal
[storefront] => USA
[storefrontId] => 143441
[price] => 100000
[currency] => USD
[appTransactionId] => 700000000000000002 // Same as previous renewal
)
[3] => Array
(
[transactionId] => 1000000000000004
[originalTransactionId] => 1000000000000001
[webOrderLineItemId] => 1000000000000004
[bundleId] => com.example.myapp
[productId] => MonthlySubscription
[subscriptionGroupIdentifier] => 20000000
[purchaseDate] => 1751646432000
[originalPurchaseDate] => 1751617840000
[expiresDate] => 1754324832000
[quantity] => 1
[type] => Auto-Renewable Subscription
[inAppOwnershipType] => PURCHASED
[signedDate] => 1751868174651
[environment] => Production
[transactionReason] => RENEWAL // Third auto-renewal
[storefront] => USA
[storefrontId] => 143441
[price] => 100000
[currency] => USD
[appTransactionId] => 700000000000000002 // Same as previous renewals
)
)
Questions
Is this behavior expected? Should the appTransactionId change between the original purchase and subsequent renewals within the same subscription group, especially when there are no user actions (upgrades/downgrades/cancellations/resubscriptions)?
What determines the appTransactionId value? The documentation doesn't clearly explain when this identifier might change or what triggers a new value. This is particularly puzzling since this is a straightforward auto-renewal scenario.
How should we handle this in our backend logic? Should we treat transactions with different appTransactionId values as separate entities, or should we rely on originalTransactionId for grouping related subscription transactions?
Is this a known issue? We've seen similar concerns in the community regarding transaction ID inconsistencies, but this specific case involves a clean auto-renewal flow without any complicating factors.
Currently, over the xcode environment to do the testing of product subscriptions through appstore are working correctly using the storeKit.
When deployed in testflight to do the testing over the integration environment, the store response times are being excessively high, in excess of 20 minutes.
This behavior is not replicated on Xcode, and is happening on recent versions uploaded to testflight, as earlier versions that were already tested and are currently in production.
In addition the communication between the appstore webhook and the BE is also failing in this environment.
It is being blocked to generate any test to be able to launch to production.
Topic:
App & System Services
SubTopic:
StoreKit
Tags:
Subscriptions
Developer Tools
App Store
StoreKit
Hello,
I recently saw this error from StoreKit in the Console - 'Invalid value for purchase intake' - while debugging a SKPayment subscription issue (where a valid receipt should be verified and restored, but isn't for one user).
I haven't been able to find any documentation about this message and wondered if it was related at all.
There were two other logs from StoreKit right before saying:
'Found 3 products in receipt with ID'
'Processing ad attribution purchase intake'
Does anyone know what 'invalid value for purchase intake' is referencing?
We don't have the AdAttributionKit implemented. It sounds like it might be related to that instead?
Thank you
Hello, I’m trying to change the business model of my app to in-app subscriptions. My goal is to ensure that previous users who paid for the app have access to all premium content seamlessly, without even noticing any changes.
I’ve tried using RevenueCat for this, but I’m not entirely sure it’s working as expected. I would like to use RevenueCat to manage subscriptions, so I’m attempting a hybrid model. On the first launch of the updated app, the plan is to validate the app receipts, extract the originalAppVersion, and store it in a variable. If the original version is lower than the latest paid version, the isPremium variable is set to true, and this status propagates throughout the app. For users with versions equal to or higher than the latest paid version, RevenueCat will handle the subscription status—checking if a subscription is active and determining whether to display the paywall for premium features.
In a sandbox environment, it seems to work fine, but I’ve often encountered situations where the receipt doesn’t exist. I haven’t found a way to test this behavior properly in production. For example, I uploaded the app to TestFlight, but it doesn’t validate the actual transaction for a previously purchased version of the app. Correct me if I’m wrong, but it seems TestFlight doesn’t confirm whether I installed or purchased a paid version of the app.
I need to be 100% sure that users who previously paid for the app won’t face any issues with this migration. Is there any method to verify this behavior in a production-like scenario that I might not be aware of?
I’m sharing the code here to see if you can confirm that it will work as intended or suggest any necessary adjustments.
func fetchAppReceipt(completion: @escaping (Bool) -> Void) {
// Check if the receipt URL exists
guard let receiptURL = Bundle.main.appStoreReceiptURL else {
print("Receipt URL not found.")
requestReceiptRefresh(completion: completion)
return
}
// Check if the receipt file exists at the given path
if !FileManager.default.fileExists(atPath: receiptURL.path) {
print("The receipt does not exist at the specified location. Attempting to fetch a new receipt...")
requestReceiptRefresh(completion: completion)
return
}
do {
// Read the receipt data from the file
let receiptData = try Data(contentsOf: receiptURL)
let receiptString = receiptData.base64EncodedString()
print("Receipt found and encoded in base64: \(receiptString.prefix(50))...")
completion(true)
} catch {
// Handle errors while reading the receipt
print("Error reading the receipt: \(error.localizedDescription). Attempting to fetch a new receipt...")
requestReceiptRefresh(completion: completion)
}
}
func validateAppReceipt(completion: @escaping (Bool) -> Void) {
print("Starting receipt validation...")
guard let receiptURL = Bundle.main.appStoreReceiptURL else {
print("Receipt not found on the device.")
requestReceiptRefresh(completion: completion)
completion(false)
return
}
print("Receipt found at URL: \(receiptURL.absoluteString)")
do {
let receiptData = try Data(contentsOf: receiptURL, options: .alwaysMapped)
print(receiptData)
let receiptString = receiptData.base64EncodedString(options: [])
print("Receipt encoded in base64: \(receiptString.prefix(50))...")
let request = [
"receipt-data": receiptString,
"password": "c8bc9070bf174a8a8df108ef6b8d2ae3" // Shared Secret
]
print("Request prepared for Apple's validation server.")
guard let url = URL(string: "https://buy.itunes.apple.com/verifyReceipt") else {
print("Error: Invalid URL for Apple's validation server.")
completion(false)
return
}
print("Validation URL: \(url.absoluteString)")
var urlRequest = URLRequest(url: url)
urlRequest.httpMethod = "POST"
urlRequest.httpBody = try? JSONSerialization.data(withJSONObject: request)
URLSession.shared.dataTask(with: urlRequest) { data, response, error in
if let error = error {
print("Error sending the request: \(error.localizedDescription)")
completion(false)
return
}
guard let data = data else {
print("No response received from Apple's server.")
completion(false)
return
}
print("Response received from Apple's server.")
do {
if let json = try JSONSerialization.jsonObject(with: data) as? [String: Any] {
print("Response JSON: \(json)")
// Verify original_application_version
if let receipt = json["receipt"] as? [String: Any],
let appVersion = receipt["original_application_version"] as? String {
print("Original application version found: \(appVersion)")
// Save the version in @AppStorage
savedOriginalVersion = appVersion
print("Original version saved in AppStorage: \(appVersion)")
if let appVersionNumber = Double(appVersion), appVersionNumber < 1.62 {
print("Original version is less than 1.62. User considered premium.")
isFirstLaunch = true
completion(true)
} else {
print("Original version is not less than 1.62. User is not premium.")
completion(false)
}
} else {
print("Could not find the original application version in the receipt.")
completion(false)
}
} else {
print("Error parsing the response JSON.")
completion(false)
}
} catch {
print("Error processing the JSON response: \(error.localizedDescription)")
completion(false)
}
}.resume()
} catch {
print("Error reading the receipt: \(error.localizedDescription)")
requestReceiptRefresh(completion: completion)
completion(false)
}
}
Some of these functions might seem redundant, but they are intended to double-check and ensure that the user is not a previous user. Is there any way to be certain that this will work when the app is downloaded from the App Store?
Thanks in advance!
Is there an App Store Server API available that allows cancellation of specific subscriptions by specifying transaction_id or similar identifiers?
Background of these questions:
We occasionally suspend user accounts due to violations of our service terms and conditions. In such cases, we would like to forcibly cancel their subscriptions if possible. However, we could not find relevant information in the documentation, which is why we are reaching out with these questions.
Please let us know.
I am experiencing an issue with my iOS app where StoreKit 2 in-app subscriptions are not appearing when using Product.products(for:). I have done all the necessary configurations, but the products still return an empty array ([]).
Here are the details of my setup:
• App ID / Bundle ID: com.tstore.vocabely
• Product IDs:
• com.tstore.vocabely.base
• com.tstore.vocabely.pro
• com.tstore.vocabely.ultimate
• Paid Apps Agreement: Active
• IAP Status in App Store Connect: Ready to Submit / Approved
• Testing Environment:
• Device: iPhone (real device)
• iOS Version: [insert your iOS version]
• Xcode Version: [insert your Xcode version]
• Sandbox Tester Account used
• Code Snippet: Using Product.products(for: productIDs) in StoreKitManager to fetch products
I have verified:
• Product IDs match exactly with App Store Connect
• Paid Apps Agreement is Active
• Using a real device, not a simulator
• Sandbox account is properly signed in
Despite all of this, fetchProducts() still returns an empty array.
Could you please assist me in troubleshooting why my subscriptions are not appearing in the Sandbox environment?
Thank you for your support.
Topic:
App & System Services
SubTopic:
StoreKit
Hi,
I'm new to AppStore Connect, and I'm learning how subscriptions work.
I'm finalizing my first app, but on my TF, I'm having some issues with the price: it's always displayed in US dollars on my paywall. When I tap "subscribe," the Apple sheet correctly offers the price in euros.
I'm using Product.displayPrice to display the price in my paywall. The documentation did mention it is
The localized string representation of the product price, suitable for display.
So, is it normal in my case for the price in dollars to be displayed and not the price in euros like the Apple sheet?
Thank you in advance for your help.
Hi,
Using StoreKit 2 with App Store Server notifications like so:
User selects a purchase
App calls Product.purchase()
If successful, App Store notifies our backend with the transaction details, importantly with a UUID for the transaction ID.
This works fine, but when I try to test contingent pricing via the handy StoreKit config Transaction Manager in Xcode by creating a PurchaseIntent if I then complete the purchase in the app the Transaction ID is sequential, (0 for the first, 1 for the second etc), which doesn't work for us as the backend might already have that ID stored so the purchase never completes.
If I disable the config file it works fine, but then I can't use the Transaction Manager debug tool. Is there a way to override the ID of a custom transaction that's created via the StoreKit configuration?
Thanks
I try to access the AppDistributor.current (using try await) and the property never seem to return nor throw.
The code I'm using looks like this:
do {
print("accessing current")
let current = try await AppDistributor.current
print("current obtained")
switch(current) {
case .appStore:
return "AppStore"
default:
return "Unknown"
}
} catch {
return "Exception: \(error)"
}
But the log only shows the accessing current and never the current obtained. Trying to step in the property starts with some assembly, but at some point, the debugger just never returned. I join a full Swift file of a sample test I'm using:
SwiftMarketplaceTests.swift
Topic:
App & System Services
SubTopic:
StoreKit
Hello all,
Posting here before I put in a support ticket to see if there are any ideas.
The previous beta issues seem to have been resolved, but now we are having intermittent problems with sandbox purchases. We do not know if this will affect real purchases. This is happening on beta 9 in both public and dev channels for us, most often on iPad Pro 4th. gen (Though idk if that is relevant).
Sometimes running TestFlight builds on iOS 26 beta 9 devices we will have attempts to make sandbox purchases just go into a black hole.
We do not get a "Do you want to buy this" popup, or the credentials screen. It just pauses for a bit in the section of our code that would be akin to:
let result = try await product.purchase( options: [.appAccountToken(accountUUID) ] )
Then wait a couple seconds, and then nothing. The game returns to normal flow as if it was a pending purchase, but nothing more ever happens. We have not been able to get a local debug build to do this, so it's hard for us to tell if it is going into the pending bucket, the userCancelled bucket, or the unverified bucket, etc.
If we take a device in this state and remove the app and reinstall from TestFlight we will get a credentials popup on the first attempt after install to buy, and after putting in our info we will get the " "You've already purchased this In-App Purchase...", but nothing ever his our listener and we return to the broken state.
Has anyone else seen issues like this?
P.S. Our StoreKit logic code is currently widely distributed, so if it was reproducible in the live version on iOS 18 we would know about it.
Thanks, Chris
Topic:
App & System Services
SubTopic:
StoreKit
I have consumable IAPs in my app. Currently there is no way for me to test refunds for them as Xcode testing doesn't allow refunds option for my Purchases. According to this official documentation on Transaction.all , i should be getting my refunded consumables in Transaction's all property.
But there is no way for me to know what kind of data is in the refunded transaction object. Will there be a 'revocation date' like in the case of non-consumables?