We are considering a price change for the auto-renewing subscriptions we currently offer in a Production environment and have made system modifications to our servers.
We would like to implement a price change for purchases made through our SANDBOX Apple account in order to test if our system is capable of handling the price change.
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
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've been unable to successfully test in the sandbox environment for a StoreKit 2 subscription group and can't seem to find the missing piece.
I am calling the following line of code:
let products = try await Product.products(for: [subscriptionID])
Expected behavior: The product is returned in the products array.
Actual result: The array is empty
I have done the following:
Successfully tested our logic using a storekit configuration file locally in Xcode.
Created the Subscription group in App Store Connect. The subscription product is currently "Waiting for Review", but it is our first so will not be approved without being attached to a distribution build review.
Created a Sandbox user account in App Store Connect -> Users -> Sandbox
Signed into the sandbox user account in Settings -> Developer -> Sandbox Apple Account
Signed the Paid Apps Agreement for our organization
A few debugging notes:
I deleted all apps before installing from Xcode
I've tried both locally and in TestFlight builds
Restarted my device
Verified productID matches the productID in App Store Connect
I'm not sure if I'm missing something, but any help would be appreciated.
Thanks
Topic:
App & System Services
SubTopic:
StoreKit
Tags:
Subscriptions
StoreKit
App Store Connect
In-App Purchase
Hello Apple Developer Team,
We're experiencing consistent IAP approval rejections under Guideline 2.1, despite successful TestFlight verification. Here's our detailed situation:
Environment
StoreKit 1 implementation
Tested on iOS 18.5 or 18.6 devices
Sandbox environment works perfectly
Verification Steps Taken
✅ Confirmed all Product IDs match App Store Connect exactly
✅ Validated 10+ successful TestFlight transactions (attached screenshot samples)
✅ Verified banking/tax agreements are active
Objective-C Code (StoreKit1 Implementation)
- (void)buyProductId:(NSString *)pid AndSetGameOrderID:(NSString *)orderID{
if([SKPaymentQueue canMakePayments]){
if (!hasAddObserver) {
[[SKPaymentQueue defaultQueue] addTransactionObserver:_neo];
hasAddObserver = YES;
}
self.neoOrderID = orderID;
[[NSUserDefaults standardUserDefaults] setValue:orderID forKey:Pay_OrderId_Key];
self.productID = pid;
NSArray * product = [[NSArray alloc]initWithObjects:self.productID, nil];
NSSet * nsset = [NSSet setWithArray:product];
SKProductsRequest * request = [[SKProductsRequest alloc]initWithProductIdentifiers:nsset];
request.delegate = self;
[request start];
}else{
NSString * Err = @"Pembelian tidak diizinkan. Silakan aktifkan perizinan di pengaturan";
// UnitySendMessage("GameManager", "IAPPurchaseFailed", [Err UTF8String]);
return;
}
}
- (void)productsRequest:(SKProductsRequest *)request didReceiveResponse:(SKProductsResponse *)response
{
NSArray * product = response.products;
if ([product count] == 0)
{
[[SKPaymentQueue defaultQueue]removeTransactionObserver:_neo];
hasAddObserver = NO;
NSString * Err = [NSString stringWithFormat:@"Err = 01, Item tidak ditemukan %@",self.productID];
// UnitySendMessage("GameManager", "IAPPurchaseFailed", [Err UTF8String]);
return;
}
SKProduct * p = nil;
for (SKProduct * pro in product)
{
if ([pro.productIdentifier isEqualToString:self.productID]){
p = pro;
}else{
[request cancel];
[[SKPaymentQueue defaultQueue]removeTransactionObserver:_neo];
hasAddObserver = NO;
NSString * Err = [NSString stringWithFormat:@"Err = 02, %@",self.productID];
// UnitySendMessage("GameManager", "IAPPurchaseFailed", [Err UTF8String]);
return;
}
}
SKMutablePayment * mPayment = [SKMutablePayment paymentWithProduct:p];
mPayment.applicationUsername = [NSString stringWithFormat:@"%@",self.neoOrderID];
if(!hasAddObserver){
[[SKPaymentQueue defaultQueue] addTransactionObserver:_neo];
hasAddObserver = YES;
}
[[SKPaymentQueue defaultQueue] addPayment:mPayment];
}
- (void)request:(SKRequest *)request didFailWithError:(NSError *)error{
[[SKPaymentQueue defaultQueue]removeTransactionObserver:_neo];
hasAddObserver = NO;
NSString * Err = [NSString stringWithFormat:@"Err = 0%ld %@", (long)error.code, self.productID];
// UnitySendMessage("GameManager", "IAPPurchaseFailed", [Err UTF8String]);
}
- (void)requestDidFinish:(SKRequest *)request{
}
- (void)paymentQueue:(SKPaymentQueue *)queue updatedTransactions:(NSArray *)transaction{
for(SKPaymentTransaction *tran in transaction){
if (SKPaymentTransactionStatePurchased == tran.transactionState){
[self completeTransaction:tran];
}else if(SKPaymentTransactionStateFailed == tran.transactionState){
[self failedTransaction:tran];
}
}
}
- (void)failedTransaction: (SKPaymentTransaction *)transaction
{
NSString * detail = [NSString stringWithFormat:@"%ld",(long)transaction.error.code];
// UnitySendMessage("GameManager", "IAPPurchaseFailed", [detail UTF8String]);
[[SKPaymentQueue defaultQueue]removeTransactionObserver:_neo];
hasAddObserver = NO;
[[SKPaymentQueue defaultQueue] finishTransaction: transaction];
}
- (void)completeTransaction:(SKPaymentTransaction *)transaction{
NSMutableDictionary * mdic = [NSMutableDictionary dictionary];
NSString * productIdentifier = transaction.payment.productIdentifier;
NSData * _recep = nil;
NSString * _receipt = @"";
if (floor(NSFoundationVersionNumber) <= NSFoundationVersionNumber_iOS_6_1) {
_recep = transaction.transactionReceipt;
_receipt = [[NSString alloc]initWithData:_recep encoding:NSUTF8StringEncoding];
} else {
_recep = [NSData dataWithContentsOfURL:[[NSBundle mainBundle] appStoreReceiptURL]];
_receipt = [_recep base64EncodedStringWithOptions:NSDataBase64EncodingEndLineWithLineFeed];
}
NSString * gameOrderid = [transaction payment].applicationUsername;
if (gameOrderid == nil) {
gameOrderid = [[NSUserDefaults standardUserDefaults] objectForKey:Pay_OrderId_Key];
}
if(_receipt != nil && gameOrderid != nil){
mdic[@"orderid"] = gameOrderid;
mdic[@"productid"] = productIdentifier;
mdic[@"receipt"] = _receipt;
}else{
[[SKPaymentQueue defaultQueue] finishTransaction:transaction];
return;
}
NSData * data = [NSJSONSerialization dataWithJSONObject:mdic options:kNilOptions error:nil];
NSString * jsonString = [[NSString alloc]initWithData:data encoding:NSUTF8StringEncoding];
if (hasAddObserver) {
[[SKPaymentQueue defaultQueue] removeTransactionObserver:_neo];
hasAddObserver = NO;
}
// UnitySendMessage("GameManager", "IAPPurchaseSuecess", [jsonString UTF8String]);
[self verifyReceipt:_recep completion:^(BOOL success, NSDictionary *response) {
if (success) {
NSLog(@"verify success");
// [[SKPaymentQueue defaultQueue] finishTransaction:transaction];
[self verifySuecessDelTransactions];
}
}];
}
- (void)paymentQueueRestoreCompletedTransactionsFinished:(SKPaymentQueue *)queue {
for(SKPaymentTransaction *tran in queue.transactions){
if (SKPaymentTransactionStatePurchased == tran.transactionState){
[self completeTransaction:tran];
}
}
}
- (void)verifySuecessDelTransactions{
SKPaymentQueue *paymentQueue = [SKPaymentQueue defaultQueue];
NSArray<SKPaymentTransaction *> *transactions = paymentQueue.transactions;
if (transactions.count == 0) {
return;
}
for (SKPaymentTransaction *transaction in transactions) {
if (transaction.transactionState == SKPaymentTransactionStatePurchased ||
transaction.transactionState == SKPaymentTransactionStateRestored) {
[paymentQueue finishTransaction:transaction];
}
}
}
Hey everyone,
I'm currently preparing an older iOS app for App Store release that includes a non-consumable In‑App Purchase using StoreKit 2. Everything works perfectly in the StoreKitTest environment inside Xcode – the product loads, the purchase flow runs, the transaction verifies.
However, when I run the same app through TestFlight, I always get the error:
❌ Product not available - mapped to
Here’s what I’ve already checked:
✅ The product ID is correct and matches what’s in App Store Connect (case-sensitive).
✅ The IAP is created in App Store Connect and includes:
Title
Product ID
Price Tier
Screenshot for review
✅ The App Store "Paid Applications" agreement is active.
✅ The app is using the correct bundle ID.
✅ I'm using Product.products(for: [productID]) from StoreKit 2.
✅ I’ve implemented fallback and retry logic (e.g. reload after delay).
✅ All IAP logic is wrapped in @MainActor and async-safe.
As the App got Rejected on Review, the IAP is also now in the Rejected Status.
Now the IAP shows status:
🟠 "Developer Action Required"
And App Review rejected the IAP with the message:
"Your first In‑App Purchase must be submitted together with a new app version."
But if I add the App to the Test again and therefore the IAP, then the app will get Rejected again for App Completeness, IAP does not work...
What am I doing wrong here? :)
Thanks a lot in advance
Cheers,
Niklas
Topic:
App & System Services
SubTopic:
StoreKit
Tags:
StoreKit Test
App Review
StoreKit
In-App Purchase
FB19377002
I am looking to improve and review my subscription purchase handling logic, for the best user experience.
Considering that StoreKit2 caches local raw transactions (in case user is offline), is it really necessary to persist "unlocked status" in UserDefaults or SwiftData Model or AppStorage? Are there significant delays when reading Transaction.currentEntitlements from locally stored cache, versus reading it from UserDefaults; or, as in the latest SKDemo example, even reading it from stored in SwiftData ?
https://developer.apple.com/forums/thread/706450
I only have subscriptions ( I don't have noncosumable or consubale products). Do I still need to persist subscription status?
FB19376771
Transactions monitoring. If I only have subscriptions, do I really need to "bother" with any sort of monitorTransactions() or just rely on subscription status (subscribed, revoked, cancelled ...) ?
This is in line with Apple SKDemo and recommendation:
// Only handle consumables and non consumables here. Check the subscription status each time
// before unlocking a premium subscription feature.
switch transaction.productType {
ref: [https://developer.apple.com/documentation/storekit/implementing-a-store-in-your-app-using-the-storekit-api)
The "Only handle consumables and non consumables here" recommendation by Apple in ref to the process transaction code above is nuanced and confusing if we know what was with other external experts recommendation saying when using only SK2 Views :
"This is where most developers trip up in trying to get an experience that App Review is happy" ...
continuing :
"Be careful: that Purchase View code alone isn’t enough, because one of the possible completion status is .pending: the purchase is in the process of happening but hasn’t completed yet, so you still need to watch the transaction queue manually to be absolutely sure of handling the process completely."
Does this holds true for the new SubscriptionStoreView ?
We are not sure with quite obscure Apple documentation what SubscriptionStoreView handles, other than purchase (and now subscribe) function, and we do not know what diverse type of error handling messages it can return. Moreover, Apple documents: "Only handle consumables and non consumables here" ?
@Apple can you please share more insights on Purchase button on SubscriptionStoreView e.g
A) does it close ( finish). the purchase transaction ?
B) What error results can it return ?
C) What .onInAppPurchaseCompletion can handle as result ?
Hey everyone,
This might be a simple fix that I’m just overlooking, but I’ve been stuck on it for the past 48 hours.
The issue is on my subscription screen — after a user completes a successful in-app purchase, the app doesn’t navigate to the main app like it’s supposed to. I’ve added logs, tried various fixes, and even asked AI for help, but nothing has worked.
From what I can tell, it seems like my listeners aren’t being registered properly after the transaction. I’ve tried reinitializing them, moving them around, and testing different flows, but still no luck.
If anyone has insight into how they’ve set this up or any suggestions I might not have considered, I’d really appreciate it.
Thanks in advance!
Dear Apple Technical Support Team,
We have encountered a potential issue related to transaction handling while using StoreKit v2, and would greatly appreciate your assistance in confirming the behavior or providing any relevant guidance.
Issue Description:
When calling Transaction.unfinished and listening to Transaction.updates on the client side, we noticed that some transactions—despite having already been processed and successfully completed with finish()—are being returned again upon the next app launch, which results in duplicate receipt uploads.
Current Handling Flow:
1. Upon app launch:
• Iterate over Transaction.unfinished to retrieve unfinished transactions;
• Simultaneously listen for transaction changes via Transaction.updates (e.g., renewals, refunds);
2. For each verified transaction, we immediately call await transaction.finish();
3. We then construct a transaction model, store it locally, and report it to our backend for receipt verification;
4. After the server successfully verifies the receipt, the client deletes the corresponding local record;
5. On every app launch, the client checks for any locally stored receipts that haven’t been uploaded, and re-uploads them if necessary.
Key Code Snippets:
private static func verifyReceipt(receiptResult: VerificationResult) -> Transaction? {
switch receiptResult {
case .unverified(_, _):
return nil
case .verified(let signedType):
return signedType
}
}
public static func handleUnfinishedTransactions(payConfig: YCStoreKitPayConfig, complete: ((YCStoreKitReceiptModel?) -> Void)?) {
Task.detached {
for await unfinishedResult in Transaction.unfinished {
let transaction = YCStoreKitV2Manager.verifyReceipt(receiptResult: unfinishedResult)
if let transaction {
await transaction.finish()
if transaction.revocationDate == nil {
let receipt = YCStoreKitV2Manager.createStoreKitReceiptModel(
transation: transaction,
jwsString: unfinishedResult.jwsRepresentation,
payConfig: payConfig,
isRenew: false
)
complete?(receipt)
}
}
}
}
}
private func observeTransactionUpdates() -> Task<Void, Never> {
return Task {
for await updateResult in Transaction.updates {
let transaction = YCStoreKitV2Manager.verifyReceipt(receiptResult: updateResult)
if let transaction {
await transaction.finish()
if transaction.revocationDate == nil {
let receipt = YCStoreKitV2Manager.createStoreKitReceiptModel(
transation: transaction,
jwsString: updateResult.jwsRepresentation,
payConfig: self.payConfig,
isRenew: false
)
self.callProgressChanged(.receiptPrepared, receiptModel: receipt, errorType: .none, error: nil)
}
}
}
}
}
Our Questions:
1. Is it possible for Transaction.unfinished or Transaction.updates to return transactions that have already been finished?
Specifically, if a transaction was successfully finished in a previous app launch, could it still be returned again during the next launch?
2. Are there any flaws in our current handling process?
Our current sequence is: finish() → construct model → local save → report to server → delete after verification. Could this order lead to timing issues where StoreKit considers a transaction unfinished?
3. If we need your assistance in investigating specific user transaction records or logs, what key information should we provide?
We greatly appreciate your support and look forward to your response to help us further optimize our transaction processing logic.
We got access into Advanced Commerce API and trying out the server APIs.
I was trying out the Migrate a Subscription to Advanced Commerce API but the API was just simply returning not found to me with a generic error code 4040000 (this is undocumented in the API doc).
Here is the request body
{
"descriptors": {
"description": "User migrated from old plan to Essential",
"displayName": "Essential Plan"
},
"items": [
{
"sku": "com.company.essential",
"description": "A new subscription description after migration",
"displayName": "Essential"
}
],
"requestInfo": {
"requestReferenceId": "11aa3174-9aeb-41a6-996d-fc655a793c06"
},
"storefront": "HKG",
"targetProductId": "com.company.subscription.base",
"taxCode": "C003-00-1"
}
Headers
Authorization: Bearer <REQUEST_TOKEN>
And the response
{
"errorCode": 4040000,
"errorMessage": "Not found."
}
Am I doing something wrong or there will be additional configuration needed?
Hi!
My product SKU has been approved for Advanced Commerce API. I successfully receive a purchase pop-up with the correct information.
However, I am still having issues with completing the purchase. I always receive Unauthorize error when I confirm the purchase (subscription in my case; see the screenshot). I am using the node.js server library to sign the request. I made sure that the account is a valid account enabled for Sandbox.
Logs unfortunately don't indicate any further detail.
Thanks for your advice! We've been stuck on this for a while now and would appreciate your help.
Marek
I have an auto-renewable subscription. I have two methods helping me keep track of when they are expired
@MainActor public func isPurchased(product: Product) async -> Bool {
guard let state = await product.currentEntitlement else {
return false
}
switch state {
case .unverified(_, _):
return false
case .verified(let transaction):
await transaction.finish()
return isTransactionRelevant(transaction)
}
}
private func isTransactionRelevant(_ transaction: Transaction) -> Bool {
if let revocationDate = transaction.revocationDate {
logger.error("Transaction verification failed: Transaction was revoked on \(revocationDate)")
return false
}
if let expirationDate = transaction.expirationDate,
expirationDate < Date()
{
logger.error("Transaction verification failed: Transaction expired on \(expirationDate)")
return false
}
if transaction.isUpgraded {
logger.error("Transaction verification failed: Transaction was upgraded")
return false
}
logger.info("Transaction verification succeeded")
return true
}
I also have this that I can call to get the latest state of purchases
@MainActor public func updateStoreKitSubscriptionStatus() async {
var currentProductsPurchased: [Product] = []
for await result in Transaction.currentEntitlements {
if case .verified(let transaction) = result {
if isTransactionRelevant(transaction) {
if let product = products.first(
where: { $0.id == transaction.productID
})
{
currentProductsPurchased.append(product)
}
}
await transaction.finish()
}
}
self.purchasedProducts = currentProductsPurchased
}
Right now when a subscription expires the user needs to manually do some action that triggers updateStoreKitSubscriptionStatus() as it appears that expirations do not come through in Transaction.updates.
I am surprised there does not seem to be a better way. Does StoreKit not notify you somewhere that an auto-renewable subscription has expired? Can you observe it in an ObservableObject? Or do I need to just frequently poll Transaction.currentEntitlements even if I dont expect frequent updates?
Topic:
App & System Services
SubTopic:
StoreKit
Whether using Storefront.current?.countryCode or SKPaymentQueue.default().storefront?.countryCode, both are returning "USA" only.
(It used to return the correct country code before the update.)
In the sandbox environment, the country code is returned correctly,
but in the TestFlight environment, it always returns "USA".
There's no mention of this behavior in the beta release notes, so I'm posting it here for visibility.
Received error that does not have a corresponding StoreKit Error: Error Domain=AMSErrorDomain Code=305 "Purchase Failed Server canceled the purchase
More details:
Error Domain=AMSErrorDomain Code=305 "Purchase Failed Server canceled the purchase" UserInfo={AMSFailureReason=Server canceled the purchase, AMSURL=https://sandbox.itunes.apple.com/WebObjects/MZBuy.woa/wa/inAppBuy?guid=00008110-000A4DC10E51401E, AMSDescription=Purchase Failed, AMSStatusCode=200, AMSServerPayload={
"cancel-purchase-batch" = 1;
customerMessage = "Unable to process your request.";
dialog = {
defaultButton = ok;
explanation = "Please try again later.\n\n[Environment: Sandbox]";
initialCheckboxValue = 1;
isFree = 1;
"m-allowed" = 0;
message = "Unable to process your request.";
okButtonString = OK;
};
failureType = "";
"m-allowed" = 0;
metrics = {
actionUrl = "sandbox.itunes.apple.com/WebObjects/MZBuy.woa/wa/inAppBuy";
asnState = 0;
dialogId = "MZCommerce.SystemError";
eventType = dialog;
message = "Unable to process your re";
mtEventTime = "2025-07-28 12:34:22 Etc/GMT";
mtTopic = "xp_its_main";
options = (
OK
);
};
pings = (
);
}, NSDebugDescription=Purchase Failed Server canceled the purchase}
Received error that does not have a corresponding StoreKit Error: Error Domain=ASDErrorDomain Code=500 "(null)" UserInfo={client-environment-type=Sandbox, storefront-country-code=IND, NSUnderlyingError=0x1276116e0 {Error Domain=AMSErrorDomain Code=305 "Purchase Failed Server canceled the purchase" UserInfo={AMSFailureReason=Server canceled the purchase, AMSURL=https://sandbox.itunes.apple.com/WebObjects/MZBuy.woa/wa/inAppBuy?guid=00008110-000A4DC10E51401E, AMSDescription=Purchase Failed, AMSStatusCode=200, AMSServerPayload={
"cancel-purchase-batch" = 1;
customerMessage = "Unable to process your request.";
dialog = {
defaultButton = ok;
explanation = "Please try again later.\n\n[Environment: Sandbox]";
initialCheckboxValue = 1;
isFree = 1;
"m-allowed" = 0;
message = "Unable to process your request.";
okButtonString = OK;
};
failureType = "";
"m-allowed" = 0;
metrics = {
actionUrl = "sandbox.itunes.apple.com/WebObjects/MZBuy.woa/wa/inAppBuy";
asnState = 0;
dialogId = "MZCommerce.SystemError";
eventType = dialog;
message = "Unable to process your re";
mtEventTime = "2025-07-28 12:34:22 Etc/GMT";
mtTopic = "xp_its_main";
options = (
OK
);
};
pings = (
);
}, NSDebugDescription=Purchase Failed Server canceled the purchase}}}
Purchase did not return a transaction: Error Domain=ASDErrorDomain Code=500 "(null)" UserInfo={client-environment-type=Sandbox, storefront-country-code=IND, NSUnderlyingError=0x1276116e0 {Error Domain=AMSErrorDomain Code=305 "Purchase Failed Server canceled the purchase" UserInfo={AMSFailureReason=Server canceled the purchase, AMSURL=https://sandbox.itunes.apple.com/WebObjects/MZBuy.woa/wa/inAppBuy?guid=00008110-000A4DC10E51401E, AMSDescription=Purchase Failed, AMSStatusCode=200, AMSServerPayload={
"cancel-purchase-batch" = 1;
customerMessage = "Unable to process your request.";
dialog = {
defaultButton = ok;
explanation = "Please try again later.\n\n[Environment: Sandbox]";
initialCheckboxValue = 1;
isFree = 1;
"m-allowed" = 0;
message = "Unable to process your request.";
okButtonString = OK;
};
failureType = "";
"m-allowed" = 0;
metrics = {
actionUrl = "sandbox.itunes.apple.com/WebObjects/MZBuy.woa/wa/inAppBuy";
asnState = 0;
dialogId = "MZCommerce.SystemError";
eventType = dialog;
message = "Unable to process your re";
mtEventTime = "2025-07-28 12:34:22 Etc/GMT";
mtTopic = "xp_its_main";
options = (
OK
);
};
pings = (
);
}, NSDebugDescription=Purchase Failed Server canceled the purchase}}}
Topic:
App & System Services
SubTopic:
StoreKit
Tags:
Subscriptions
StoreKit Test
StoreKit
In-App Purchase
My Mac app fails to open for some users with the error:
"ABC.app does not support the latest receipt validation requirements."
I assume this is due to the update of the App Store receipt signing intermediate certificate with one that uses the SHA-256 algorithm.
I cannot reproduce this myself and I have trouble figuring out how to address this issue.
Below is the code that decrypts the receipt and verifies its signature.
How does this code need to be updated to support the new signing certificate?
Thanks a lot in advance!
inline static void CheckBundleSignature(void)
{
NSURL *bundleURL = [[NSBundle mainBundle] bundleURL];
SecStaticCodeRef staticCode = NULL;
OSStatus status = SecStaticCodeCreateWithPath((__bridge CFURLRef)bundleURL, kSecCSDefaultFlags, &staticCode);
if (status != errSecSuccess) {
[NSException raise:@"MacAppStore Receipt Validation Error" format:@"Failed to validate bundle signature: Create a static code", nil];
}
NSString *requirementText = @"anchor apple generic";
SecRequirementRef requirement = NULL;
status = SecRequirementCreateWithString((__bridge CFStringRef)requirementText, kSecCSDefaultFlags, &requirement);
if (status != errSecSuccess) {
if (staticCode) CFRelease(staticCode);
[NSException raise:@"MacAppStore Receipt Validation Error" format:@"Failed to validate bundle signature: Create a requirement", nil];
}
status = SecStaticCodeCheckValidity(staticCode, kSecCSDefaultFlags, requirement);
if (status != errSecSuccess) {
if (staticCode) CFRelease(staticCode);
if (requirement) CFRelease(requirement);
[NSException raise:@"MacAppStore Receipt Validation Error" format:@"Failed to validate bundle signature: Check the static code validity", nil];
}
if (staticCode) CFRelease(staticCode);
if (requirement) CFRelease(requirement);
}
static NSData *DecodeReceiptData(NSData *receiptData)
{
CMSDecoderRef decoder = NULL;
SecPolicyRef policyRef = NULL;
SecTrustRef trustRef = NULL;
@try {
OSStatus status = CMSDecoderCreate(&decoder);
if (status) {
[NSException raise:@"MacAppStore Receipt Validation Error" format:@"Failed to decode receipt data: Create a decoder", nil];
}
status = CMSDecoderUpdateMessage(decoder, receiptData.bytes, receiptData.length);
if (status) {
[NSException raise:@"MacAppStore Receipt Validation Error" format:@"Failed to decode receipt data: Update message", nil];
}
status = CMSDecoderFinalizeMessage(decoder);
if (status) {
[NSException raise:@"MacAppStore Receipt Validation Error" format:@"Failed to decode receipt data: Finalize message", nil];
}
NSData *ret = nil;
CFDataRef dataRef = NULL;
status = CMSDecoderCopyContent(decoder, &dataRef);
if (status) {
[NSException raise:@"MacAppStore Receipt Validation Error" format:@"Failed to decode receipt data: Get decrypted content", nil];
}
ret = [NSData dataWithData:(__bridge NSData *)dataRef];
CFRelease(dataRef);
size_t numSigners;
status = CMSDecoderGetNumSigners(decoder, &numSigners);
if (status) {
[NSException raise:@"MacAppStore Receipt Validation Error" format:@"Failed to check receipt signature: Get singer count", nil];
}
if (numSigners == 0) {
[NSException raise:@"MacAppStore Receipt Validation Error" format:@"Failed to check receipt signature: No signer found", nil];
}
policyRef = SecPolicyCreateBasicX509();
CMSSignerStatus signerStatus;
OSStatus certVerifyResult;
status = CMSDecoderCopySignerStatus(decoder, 0, policyRef, TRUE, &signerStatus, &trustRef, &certVerifyResult);
if (status) {
[NSException raise:@"MacAppStore Receipt Validation Error" format:@"Failed to check receipt signature: Get signer status", nil];
}
if (signerStatus != kCMSSignerValid) {
[NSException raise:@"MacAppStore Receipt Validation Error" format:@"Failed to check receipt signature: No valid signer", nil];
}
return ret;
} @catch (NSException *e) {
@throw e;
} @finally {
if (policyRef) CFRelease(policyRef);
if (trustRef) CFRelease(trustRef);
if (decoder) CFRelease(decoder);
}
}
I cannot explain how frustrating this is. Not that I want to compare to Android, but in 3 years of QA Testing my app, Android works like a dream, while iOS fights with me EVERY SINGLE STEP OF THE WAY.
Hopefully someone here can tell me what I am missing/doing wrong/which god I must appease to get this to work.
I have 3 REAL iPhones of varying iOS versions and ages. But they are all proper actual iPhones. We use google accounts at this company, so my primary email is a gmail one.
I have created MANY sandbox accounts inside App Store Connect. Currently I have 2, and 2 of my devices (both 14's one of which is a Pro) have my Primary account as the main account for the device. But they both also have a Sandbox account which is simply my main email with a +sandbox in it to make it a new unique email.
Here is the problem, nothing works as expected ever. I can install my Staging and Production apps from TestFlight, then I can make a subscription purchase as a customer would and I SHOULD see that subscription in my Sandbox right? That's the point of a Sandbox and TestFlight is it not? But in ALL cases whenever I try to view my 'Sandbox Subscriptions' it tells me I don't have any.
Now, sometimes, very occasionally, I get a specific error message inside my app when attempting to make a purchase, this one states something like 'You already have a subscription, please restore it instead...' which makes no sense. Since it clearly states that I have none. But this message has a 'Manage' button to manage my subscriptions, tapping it lads me to a windows which amazing DOES have a subscription in it. But attempting to 'Cancel' it does nothing, just refreshes the screen to be the same.
Now I think that this subscription is actually attached to the primary account on the device and NOT the sandbox account. So when this happens I cannot subscribe, I cannot restore, and I cannot manually alter the subscription within iOS. So I am stuck at this point.
What am I doing wrong, am I setting this all up in the wrong order? Do I need to install some kind of profile or security cert, do I need to give a pint of blood to Imhotep? What am I missing. I even once sat on the phone for 90 minutes with an Apple Support Rep who took me through it step by step, same result.
Also I just noticed that inside 'App Store Connect' when you look at the list of 'Sandbox' accounts there is a column for 'Last Purchase' which is entirely blank, apparently after a year of use I have NEVER purchased on the Sandbox, which is another reason I think my subs are going to the main email, not the sandbox one. I tried using the sandbox email as the main account for the whole device, I can't recall the result but it was worse and didn't work at all. So that's not it.
https://developer.apple.com/help/app-store-connect/test-in-app-purchases/create-a-sandbox-apple-account/ The instructions on this page are not detailed enough and were not helpful to me.
All I really want to know is how to fully setup a real actual iPhone for TestFlight and Sandbox testing of a app. WHat order do I create accounts, validate emails, attach to devices, login with etc etc etc. Step by step, nothing no matter how mundane missed out. A true idiots guide to making this work for me. Testing this on Android always takes 5 mins. iPhone, I'm lucky if I am done in half a day.
Please help and thanks for reading!
I have created a Python app and built it with pyinstaller and codesigned everything. Now I want to Sandbox test it. In my appstore connect account i have created a subscriptions id. I read that if I am logged out from the AppStore and have codesigned my .app file with a Developer Certificate i should be able to run the app on my local mac and when i click on the "Buy" button it should connect to my app store connect setup. I have implemented StoreKit in my app and use a storekit_bridge to combine the .swift code with my python app.
However when i run the app. I get this: "25-07-24 21:01:12,557 - FEC - WARNING - StoreKit: fetchProducts returned empty result
2025-07-24 21:01:12,557 - FEC - INFO - StoreKit fetch_products returned: {"products": []}
2025-07-24 21:01:12,557 - FEC - ERROR - StoreKit: Failed to parse product info: No products returned from JSON"
And no login screen appears where I should be able to enter my Sandbox email adress and password.
Anyone here who has experience with a Python app combined with In App Purchases? Hope someone can help me out with this.
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?
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.
On iOS 26 beta 3, after a user purchases an item, initiating a second order for the same product fails to process payment. The system returns the same transaction ID and displays an interface message stating: "You've already purchased this In-App Purchase. It will be restored for free."
I’ve tested this – not only did the legacy StoreKit finishTransaction method fail to work, but StoreKit2 finish method also malfunctioned.
When will Apple fix this issue? If unresolved, it will prevent a large number of users from making purchases normally, leading to disastrous consequences.