'Guideline 2.1 - Performance - App Completeness' - "...could not be found in the submitted binary."
I have checked with internal and external testers and my devices and simulators, everyone sees the in app purchases but I just had my submitted rejected for the second time with the comment that these in-app none-consumable purchases cannot be found with the submitted binary.
I even attached a slow step by step screen recording for the review reply after the first rejection showing how to reach the purchasable packs by navigating through only 3 buttons:
"How to access the purchase flow:
Launch the app
Tap the bottom-center Settings button (icon: switch.2)
Tap “Customisation gallery”
Scroll to find any pack listed above
Tap the pack price chip
Tap “Buy pack – [price]” to start the StoreKit purchase flow"
I also attached a clear image along with detailed instruction (same as above) for the Review Information. and the second rejection was received today for the same reason.
I'm being guided to the localization 'Developer Action Needed'. I'm not sure what more can be done? I feel like my review replies aren't even looked at.
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
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
"In iTunes IAP space"
Give a monthly subscription with 7 days freeTrail, what would be sequence of iTunes V2 notification for the following behaviour?
When an end user purchases a subscription that includes a free trial.
When the user transitions from the free‑trial period to the paid subscription period.
Hello,
Our app is approved for the Advanced Commerce API and we are currently testing in the Sandbox environment only.
We have created generic product identifiers and have already submitted them via the Advanced Commerce API Access form.
However, the generic product status in App Store Connect is still “Ready to Submit.”
For Sandbox testing, is this status expected, or do we need to submit an app build or the generic product for review before Advanced Commerce works correctly?
Thank you.
Topic:
App & System Services
SubTopic:
StoreKit
Tags:
Subscriptions
In-App Purchase
Advanced Commerce API
I has sandbox account with Japanese local. When i build app directly to check, price is displayed in Japanese Currency. But when I install app from the Test Flight, price is always displayed in USD Currency.
the issue is appear in iOS 18.5
How can i fix this issue ?
Hello,
I’m experiencing repeated rejections related to Guideline 2.1 – App Completeness for an iOS app using auto-renewable subscriptions, and I’m struggling to understand what is missing, as the purchase flow works correctly in sandbox and TestFlight.
App setup:
iOS app built with React Native (Expo + react-native-iap)
Auto-renewable subscriptions:
• Monthly: €4.99
• Yearly: €39.99
Paid Apps Agreement accepted
Subscriptions configured and active in App Store Connect
Privacy Policy and Apple Standard EULA included:
• Visible inside the app on the subscription screen
• Added in App Store metadata
What App Review reports:
App Review states they are unable to buy the in-app purchase, resulting in a rejection under Guideline 2.1 (App Completeness).
What works correctly:
getSubscriptions() returns valid products in sandbox
Subscription titles, prices, and durations are displayed in the app UI
requestSubscription() is triggered when tapping the subscribe button
Apple purchase sheet appears and completes successfully in:
• Sandbox testing
• TestFlight (external testers)
What I’ve verified:
No conditional logic blocks purchases in review builds
Purchase button always calls requestSubscription
purchaseUpdatedListener and purchaseErrorListener are correctly registered
No hardcoded prices; prices come from StoreKit
Same behavior on iPhone and iPad
Question:
Is there any known limitation or requirement in the App Review environment for auto-renewable subscriptions that differs from sandbox/TestFlight when using a custom subscription UI (not SubscriptionStoreView)?
If App Review requires a specific implementation detail (StoreKit 2, SubscriptionStoreView, or something else), I would really appreciate clarification, as this is not explicitly stated in the rejection.
Thank you for your help.
I implemented consumable in-app purchases in an iPhone app using StoreKit's ProductView().
When I tap the payment button in ProductView(), I am taken to the payment screen and once the payment is completed, the desired code appears to be executed, so there doesn't seem to be a problem, but when I tap the payment button in ProductView() again, the desired code is executed without being taken to the payment screen.
So one payment can be used any number of times.
I thought I wrote it exactly according to the reference, but
will it be okay in a production environment?
Is there any code that is necessary?
Background:
My app uses a third-party SDK for payments, and it uses Original StoreKit internally for IAP payments. Now I'm getting ready to migrate to StoreKit2, and during the transition, users may use either method to initiate payments, and there's no way to avoid the coexistence of StoreKit2 and Original StoreKit.
Problem:
When a user has an unfinished transaction, if the app is restarted, both StoreKit2 and Original StoreKit will receive a notification of the transaction:
Original StoreKit's '-paymentQueue:updatedTransactions:' method
StoreKit2's 'Transaction.updated' method
resulting in duplicate calls to the shipping API.
My current treatment is to only add '-paymentQueue:updatedTransactions:' to listen for unfinished transactions. Even if the user is using StoreKit2 to initiate the payment, if the transaction is not Finished, it will be fetched via this method after restarting the app to process this transaction.
Is this approach feasible and are there any best practices for this scenario?
To summarize:
Is it feasible to fetch unfinished StoreKit2 transactions via Original StoreKit methods when StoreKit2 coexists with Original StoreKit? Is there a recommended way
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. 😕
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!
My app has a couple of consumable IAP items. I have tested this extensively and it works in all test scenarios including loads of beta testers using testflight. However, Apple's production reviewer reports that loading of the products hangs in their setup.
This is very frustrating as I have no means of recreating the problem.
My first product was tested ok an all my IAP items are approved for release. However, I did not explicitly assign them to my build. I read somewhere that you need to do that but could not find in App Store Connect after my first product was approved.
Below is the relevant code section. What am I missing?
class DonationManager: NSObject, ObservableObject, SKProductsRequestDelegate, SKPaymentTransactionObserver {
@Published var products: [SKProduct] = [] // This is observed by a view. But apparently that view never gets populated in Apple's production review setup
@Published var isPurchasing: Bool = false
@Published var purchaseMessage: String? = nil
let productIDs: Set<String> = ["Donation_5", "Donation_10", "Donation_25", "Donation_50"]
override init() {
super.init()
SKPaymentQueue.default().add(self)
fetchProducts()
}
deinit {
SKPaymentQueue.default().remove(self)
}
func fetchProducts() {
print("Attempting to fetch products with IDs: \(productIDs)")
let request = SKProductsRequest(productIdentifiers: productIDs)
request.delegate = self
request.start()
}
func productsRequest(_ request: SKProductsRequest, didReceive response: SKProductsResponse) {
DispatchQueue.main.async {
self.products = response.products.sorted { $0.price.compare($1.price) == .orderedAscending }
print("Successfully fetched \(self.products.count) products.")
if !response.invalidProductIdentifiers.isEmpty {
print("Invalid Product Identifiers: \(response.invalidProductIdentifiers)")
self.purchaseMessage = NSLocalizedString("Some products could not be loaded. Please check App Store Connect.", comment: "")
} else if self.products.isEmpty {
print("No products were fetched. This could indicate a problem with App Store Connect configuration or network.")
self.purchaseMessage = NSLocalizedString("No products available. Please try again later.", comment: "")
}
}
}
...and the view showing the items:
@StateObject private var donationManager = DonationManager()
var body: some View {
VStack(spacing: 24) {
Spacer()
// Donation options -------------------
if donationManager.products.isEmpty {
ProgressView(NSLocalizedString("Loading donation options...", comment: ""))
.foregroundColor(DARK_BROWN)
.italic()
.font(.title3)
.padding(.top, 16)
} else {
ForEach(donationManager.products, id: \.self) { product in
Button(action: {
donationManager.buy(product: product)
}) {
HStack {
Image(systemName: "cup.and.saucer.fill")
.foregroundColor(.pink)
Text("\(product.localizedTitle) \(product.priceLocale.currencySymbol ?? "$")\(product.price)")
}
.buttonStyle()
}
.disabled(donationManager.isPurchasing)
}
}
We have encountered an issue when verifying transactions using the Get Transaction Info API.
We tested the behavior in both the sandbox and production environments and observed the following results.
When calling the production endpoint:
https://api.storekit.itunes.apple.com/inApps/v1/transactions/{transactionId}
with a transactionId generated in the sandbox environment, the API returns HTTP 401 Unauthorized.
However, based on the documentation and common understanding, we expected HTTP 404 Not Found in this case.
Using the same JWT token, if we call the sandbox endpoint:
https://api.storekit-sandbox.itunes.apple.com/inApps/v1/transactions/{transactionId},
we receive HTTP 200 OK with the expected response body.
We have also confirmed that the same behavior occurs when using the Get Transaction History API — it works correctly in the sandbox environment but returns 401 in production.
Could you please confirm whether this behavior (receiving 401 instead of 404) is expected by design, or if it indicates a potential issue?
If this is not the intended behavior, we would appreciate any guidance or instructions to resolve it.
Thank you very much for your technical support.
「Get Transaction Info」APIを用いてトランザクションの検証を行ったところ、以下の問題が発生しました。
サンドボックス環境および本番環境の両方で検証を行い、次の結果を確認しています。
本番環境エンドポイント https://api.storekit.itunes.apple.com/inApps/v1/transactions/{transactionId}
に対して サンドボックス環境で生成された transactionId を使用すると、HTTP 401 Unauthorized が返却されます。
(一般的には、この場合 404 Not Found が返る想定であると理解しています。)
同一のJWTトークン を用いて サンドボックス環境のエンドポイント
https://api.storekit-sandbox.itunes.apple.com/inApps/v1/transactions/{transactionId}
を呼び出した場合は、HTTP 200 OK が返り、期待通りのレスポンスボディを受け取ることができています。
また、同様の挙動が Get Transaction History を使用した場合にも発生することを確認しています。
サンドボックス環境では正常に動作しますが、本番環境では401が返却されます。
この挙動(401が返却されること)は仕様上想定されたものか、または何らかの問題によるものかご確認をお願いいたします。
もし想定外の挙動である場合は、解決に向けたご案内をいただけますと幸いです。
本件について、技術的なサポートをお願いいたします。
よろしくお願いいたします。
We have some users who have upgraded to iOS 26 beta3. Currently, we observe that when these users make in-app purchases, our code calls [[SKPaymentQueue defaultQueue] finishTransaction:transaction]; method, and we clearly receive the successful removal callback in the delegate method - (void)paymentQueue:(SKPaymentQueue *)queue removedTransactions:(NSArray<SKPaymentTransaction *> *)transactions. However, when users click on products with the same productId again, the method - (void)paymentQueue:(SKPaymentQueue *)queue updatedTransactions:(NSArray *)transactions still returns information about previously removed transactions, preventing users from making further in-app purchases.
Description
SKTestSession.setSimulatedError() does not throw the configured error when testing StoreKit with the .loadProducts API in iOS 26.2. The simulated error is ignored, and products load successfully instead.
Environment
iOS: 26.2 (Simulator)
Xcode: 26.2 beta 2 (Build 17C5038g)
macOS: 15.6.1
Framework: StoreKitTest
Testing Framework: Swift Testing
base project: https://developer.apple.com/documentation/StoreKit/implementing-a-store-in-your-app-using-the-storekit-api
Expected Behavior
After calling session.setSimulatedError(.generic(.notAvailableInStorefront), forAPI: .loadProducts), the subsequent call to Product.products(for:) should throw StoreKitError.notAvailableInStorefront.
Actual Behavior
The error is not thrown. Products load successfully as if setSimulatedError() was never called.
Steps to Reproduce
Create an SKTestSession with a StoreKit configuration file
Call session.setSimulatedError(.generic(.notAvailableInStorefront), forAPI: .loadProducts)
Call Product.products(for:) with a valid product ID
Observe that no error is thrown and the product loads successfully
Sample Code
import StoreKitTest
import Testing
struct SKDemoTests {
let productID: String = "plus.standard"
@Test
func testSimulatedErrorForLoadProducts() async throws {
let storeKitConfigURL = try Self.getStoreKitConfigurationURL()
let session = try SKTestSession(contentsOf: storeKitConfigURL)
try await session.setSimulatedError(
.generic(.notAvailableInStorefront),
forAPI: .loadProducts
)
try await confirmation("StoreKitError throw") { throwStoreKitError in
do {
_ = try await Self.execute(productID: productID)
} catch let error as StoreKitError {
guard case StoreKitError.notAvailableInStorefront = error else {
throw error
}
throwStoreKitError()
} catch {
Issue.record(
"Expect StoreKitError. Error: \(error.localizedDescription)"
)
}
}
#expect(session.allTransactions().isEmpty)
}
static func execute(productID: String) async throws {
guard
let product = try await Product.products(for: [productID]).first(
where: { $0.id == productID })
else {
throw NSError(
domain: "SKDemoTests",
code: 404,
userInfo: [NSLocalizedDescriptionKey: "Product not found for ID: \(productID)"]
)
}
_ = product
}
static func getStoreKitConfigurationURL() throws -> URL {
guard
let bundle = Bundle(identifier: "your.bundle.identifier"),
let url = bundle.url(forResource: "Products", withExtension: "storekit")
else {
fatalError("StoreKit configuration not found")
}
return url
}
}
Test Result
The test fails because the expected StoreKitError.notAvailableInStorefront is never thrown.
Question
Is this a known issue in iOS 26.2 / Xcode 26.2 beta 2, or is there a different approach required for simulating errors with SKTestSession in this version? Any guidance would be appreciated.
Feedback Assistant report: FB21110809
I have implemented IAP. The purchases are successful. The refresh receipt is working fine, which then calls the requestDidFinish(_ request: SKRequest) delegate. I'm fetching the receipt url through 'Bundle.main.appStoreReceiptURL'. When I convert the receipt data in base64 string and send it to app store's sandbox api and try to validate the receipt, it fails giving status code : 21002.
Some paid users are unable to use the paid features unlocked by purchasing our subscription plan. It seems that this is due to StoreKit 2's Transaction.currentEntitlements not working the way we would expect it to work.
Are you also encountering this issue? Do you have any idea to improve this situation?
At launch, our app checks if the user is subscribed to the plan, using Transaction.currentEntitlements. As a result, the currentEntitlements array was empty.
Our app then fetches the products from StoreKit 2 using Product.products(for:). As a result, the Product.SubscriptionInfo.RenewalState value of the corresponding Product (product.subscription.status.first.state) is subscribed, which confirms that the user has indeed purchased our plan, but seems to contradict the absence of the corresponding transaction in Transaction.currentEntitlements.
Proactive in-app purchase restore and a restore purchase button calling the AppStore.sync() method are implemented, but using the button did not solve the issue.
I am shown as being subscribed to our service in the Subscriptions list in settings yet when going to the Storekit2 page in my app it shows me as NOT being subscribed and is unresponsive. I select Restore Subscription, that grays briefly, asks for a password, then returns to blue and nothing else happens. Bouncing back and forth between monthly and yearly likewise gives no response.
The Transaction.currentEntitlements seems to be empty so it thinks the user is not subscribed.
I have unsubscribed, and resubscribed via the Settings page to no avail.
Topic:
App & System Services
SubTopic:
StoreKit
SKProductsRequest always returns as USD not local currency for debug environment and even some time it fails. This is only happening for debug or TestFlight build.
Hi All,
We are developing our app with an approved External Link Account Entitlement.
During the development process (such as installing from Xcode or creating an Ad-hoc build and installing it on a phone), the open() function of the External Link Account API displays the modal our native language. The app only localized to that language.
However, after uploading the app with the same configuration to TestFlight, the modal somehow appears in English instead.
What could be causing this issue with the External Link Account modal? How can the open() function display the modal in another language when installed from Xcode or an Ad-hoc release build, but in English when installed from TestFlight? How can we show only our native lanugage version only to our Users?
Thank you in advance
Hello,
We are having an issue with the RequestReview API and were hoping to get some help. We know that there is no guarantee that the in-app review modal will show and we know that there are 3 circumstances in which it will definitely not appear:
if the user has turned off in-app review/ratings in their settings
if the user has submitted a review for that app on that device within the last 365 days
if the user has been asked for a review >3 times in the last 365 days
When testing our implementation, every single one of our testers did not receive the rating modal despite the fact that we had all our testers turn on the app rating setting and that we have never asked for reviews from our app before. So that seems suspicious. While it is possible that something is up with our code (and I have provided some snippets below) we are also concerned that apple maybe is suppressing it for another reason. We really want to go live with our app review code but unfortunately we are not able to get confidence that it will ever appear for the user. Can you please help us understand why this isn't working.
The code: We are using the SwiftUI approach to requesting review. Here are some relevant code snippets
Important to note, we have a modal that appears when the user is in our list of active, targeted users. If they tap yes on this modal, it should show the in app rate the app system modal. If they tap no, we present them with an airship survey so that they can give feedback. Here is the code for the Yes button action:
@Environment(\.requestReview) private var requestReview
private var yesButton: some View {
Button(
action: {
dismiss()
requestReview()
},
label: {
Text(Lingua.General.appRateFirstButton)
.regularParagraph()
.frame(width: 180, height: 35)
}
)
.customButtonStyle(
foregroundColor: .black,
backgroundColor: Color(.powderBlue),
radius: 36
)
}
and this is the logic we use to determine whether we want to show them the modal in the first place. Obviously, a lot of this code leads to deeper areas in our logic and code but to give an idea...
private func showAppRateModalIfNeeded() {
if preferencesManager.appRateReviewShown == nil,
accountManager.userAccount?.permissions.rateTheApp == true {
let appReviewModalVC = UIHostingController(rootView: AppReviewModal())
appReviewModalVC.view.backgroundColor = .init(white: 0, alpha: 0.6)
appReviewModalVC.modalPresentationStyle = .overFullScreen
appReviewModalVC.modalTransitionStyle = .crossDissolve
parentVC?.navigationController?.present(appReviewModalVC, animated: true)
preferencesManager.appRateReviewShown = true
}
}
When testing in debug, we do find that the modal appears and works as expected. However, on release builds nobody is able to trigger it. Why? Are we doing something wrong here or is Apple just suppressing it. We are thinking about implementing the button taking the user directly into the app store review but we'd prefer to do the lower-friction dialog in-app if we can get it work so the user doesn't get sent out of the app.
Topic:
App & System Services
SubTopic:
StoreKit