StoreKit

RSS for tag

Support in-app purchases and interactions with the App Store using StoreKit.

StoreKit Documentation

Posts under StoreKit subtopic

Post

Replies

Boosts

Views

Activity

My iOS application cannot connect to the Sandbox environment.
I am testing the subscription flow in my iOS app. Initially, everything was working fine when following the official StoreKit and sandbox testing documentation. After a successful subscription, the “You’re all set” popup always displayed the environment as “sandbox.” However, after some changes, possibly upgrading macOS to the latest version, upgrading Xcode, or regenerating certificates, I can no longer connect to the sandbox testing environment. The subscription success popup now always shows the environment as “xcode.” By default, the iOS app should run in the sandbox on macOS, so I didn’t set the “Enable App Sandbox” option to “Yes” in the Xcode build settings. When I try enabling it, Xcode throws the following error: “Failed to verify code signature of /var/installd/Library/Caches/com.apple.mobile.installd.staging/temp.n3J0tr/extracted/Payload/XXXX.app : 0xe8008015 (A valid provisioning profile for this executable was not found.) Please ensure that your app is signed by a valid provisioning profile.” Additionally, if “Enable App Sandbox” is set to “No,” the app installs successfully on a real device, but there is no prompt to trust an untrusted developer certificate, which usually appears for such certificates. I’m not sure if this information will be useful to others, but I’ve been stuck on this issue for a while, and it’s preventing me from moving forward with my work. Any help to resolve this would be greatly appreciated. Thank you!
3
0
319
Feb ’25
original_transaction_id associated to web_order_line_item_id of auto-renewable subscription changed
Our app offers auto-renewable subscriptions using StoreKit Original API for In-App Purchase and App Store Server Notifications V1. Starting around 2025-03-15, we found some cases where original_transaction_id which was associated to web_order_line_item_id of already purchased subscription had changed in the receipt information of verifyReceipt response or App Store Server Notifications V1. The detailed steps are: Around February 2025, re-purchase from the app the same subscription product which was canceled and expired some time ago, using StoreKit Original API for In-App Purchase Receive the following 2 notifications from App Store Server Notifications V1 almost at the same time INITIAL_BUY DID_CHANGE_RENEWAL_STATUS In both notifications, latest_receipt_info contains the receipt for the re-purchased subscription period with new original_transaction_id and web_order_line_item_id pending_renewal_info contains both new original_transaction_id and original one which was generated at first purchase, and original one has "is_in_billing_retry_period": "1" Starting around 2025-03-15, the following happens When we receive another "DID_CHANGE_RENEWAL_STATUS" notification from App Store Server Notifications V1, original_transaction_id which is associated to web_order_line_item_id of the re-purchased subscription period has changed back to the original one (the one which was generated at first purchase) in latest_receipt_info. When we call verifyReceipt with the receipt obtained from appStoreReceiptURL, the response does not seem to contain new original_transaction_id which was generated at re-purchase We have some questions regarding this original_transaction_id behavior. When a user re-purchase the same subscription product which was canceled and expired some time ago, it seems that new original_transaction_id is generated. Is this an expected behavior? If yes, it seems that, at some point, original_transaction_id which is associated to web_order_line_item_id of the re-purchased subscription changed back to the original original_transaction_id which had been generated at first purchase. Is this an expected behavior? What triggers the original_transaction_id change to the original one? Is it related to some user actions or subscription status change?
3
0
341
Mar ’25
Unable to Authenticate with App Store Server API in Production (401 Error)
Our application is currently under review, and we are still facing issues because we receive a 401 Unauthorized response from the App Store Connect API when using the production environment. Our app integrates with Chargebee for subscription management, and in production, Chargebee is unable to authenticate with the App Store Server API. This results in a 401 Unauthorized error, preventing the user’s subscription from being synced correctly into our system. Interestingly, the same configuration works in the sandbox environment, but fails in production. We’ve tried authenticating using JWTs generated from multiple keys (including App Store Connect API / Team Keys with both Admin and App Manager access, and also In-App Purchase keys), all with the same result — sandbox access works, production does not. Here is our example code for testing with JWT token: const jwt = require('jsonwebtoken'); const fs = require('fs'); const https = require('https'); const config = { keyId: '<key_id>', issuerId: 'issuer_id', bundleId: 'bundle_id', privateKey: fs.readFileSync('path_to_key') }; const { keyId, issuerId, bundleId, privateKey } = config; const now = Math.floor(Date.now() / 1000); const jwtToken = jwt.sign( { iss: issuerId, iat: now, exp: now + 60 * 10, // 10 minutes is fine for test aud: 'appstoreconnect-v1', bid: bundleId }, privateKey, { algorithm: 'ES256', header: { alg: 'ES256', kid: keyId, typ: 'JWT' } } ); console.log('Generated JWT:\n', jwtToken); // prod const originalTransactionId = '<prod_transaction_id>'; const hostname = 'api.storekit.itunes.apple.com'; // sandbox // const originalTransactionId = '<sandbox_transaction_id>'; // const hostname = 'api.storekit-sandbox.itunes.apple.com' const options = { hostname, port: 443, path: `/inApps/v1/history/${originalTransactionId}`, method: 'GET', headers: { Authorization: `Bearer ${jwtToken}`, 'Content-Type': 'application/json', }, }; const callAppStoreConnectApi = async () => { const req = https.request(options, (res) => { console.log(`\nStatus Code: ${res.statusCode}`); let data = ''; res.on('data', (chunk) => { data += chunk; }); res.on('end', () => { console.log('Response Body:\n', data || '[Empty]'); }); }); req.on('error', (e) => { console.error('Request Error:', e); }); req.end(); }; callAppStoreConnectApi(); With this code, we were able to authenticate successfully in the sandbox environment, but not in production. I read in this discussion: https://developer.apple.com/forums/thread/711801 that the issue was resolved once the app was published to the App Store, but I haven’t found any official documentation confirming this. Does anyone know what the issue could be?
3
2
280
Oct ’25
StoreKit2 - Maintain property of purchased products
Hi everybody! I'm desperately looking for help as I'm stuck with a rather fundamental problem regarding StoreKit2 - and maybe Swift Concurrency in general: While renovating several freemium apps I'd like to move from local receipt validation with Receigen / OpenSSL to StoreKit2. These apps are using a dedicated "StoreManager" class which is encapsulating all App Store related operations like fetching products, performing purchases and listening on updates. For this purpose the StoreManager holds an array property with IDs of all purchased products, which is checked when a user invokes a premium function. This array can have various states during the app's life cycle: Immediately after app launch (before the receipt / entitlements are checked) the array is empty After checking the receipt the array holds all (locally registered) purchases Later on it might change if an "Ask to Buy" purchase was approved or a purchase was performed It is important that the array is instantly used in other (Objective-C) classes to reflect the "point in time" state of purchased products - basically acting like a cache: No async calls, completion handler, notification observer etc. When moving to StoreKit2 the same logic applies, but the relevant API calls are (of course) in asynchronous functions: Transaction.updates triggers Transaction.currentEntitlements, which needs to update the array property. But Xcode 16 is raising a strict error because of potential data races when accessing the instance variable from an asynchronous function / actor. What is the way to propagate IDs of purchased products app-wide without requiring every calling function as asynchronous? I'm sure I'm missing a general point with Swift Concurrency: Every example I found was working with call-backs / await, and although this talk of WWDC 2021 is addressing "protecting mutable states" I couldn't apply its outcomes to my problem. What am I missing?
3
0
624
Jan ’25
Sandbox apple in app purchases not working
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}}}
3
0
211
Jul ’25
Potential Issue Identified in Apple Documentation
While reviewing the Apple Documentation, I came across a potential issue in one of the examples that I believe is worth addressing. The example appears to compare strings instead of integers, which could lead to unexpected behavior in production environments. Specifically, in the line where originalMajorVersion (a string) is compared with newBusinessModelMajorVersion (also a string) using <: if originalMajorVersion < newBusinessModelMajorVersion This comparison performs a lexicographical check rather than evaluating the numerical values of the strings. As a result, strings like "10" would incorrectly be considered less than "2", which is not the desired behaviour when comparing version numbers. I have reported this via the Feedback assistant (FB16432337) but at the time of posting this there has been no reply at all (23 days) Supporting business model changes by using the app transaction do { // Get the appTransaction. let shared = try await AppTransaction.shared if case .verified(let appTransaction) = shared { // Hard-code the major version number in which the app's business model changed. let newBusinessModelMajorVersion = "2" // Get the major version number of the version the customer originally purchased. let versionComponents = appTransaction.originalAppVersion.split(separator: ".") let originalMajorVersion = versionComponents[0] if originalMajorVersion < newBusinessModelMajorVersion { // This customer purchased the app before the business model changed. // Deliver content that they're entitled to based on their app purchase. } else { // This customer purchased the app after the business model changed. } } } catch { // Handle errors. }
3
0
325
Feb ’25
In-app purchase for auto-renewals subscription for sandbox
Dear Apple Support Team, We are currently implementing auto-renewable subscriptions in our iOS app and are testing the integration using the sandbox environment. On the iOS app side, the in-app purchase flow completes successfully and displays a "Purchase Successful" message. However, we are not receiving any server notification callbacks on our configured App Store Server Notifications (Sandbox) webhook URL. For your reference, the webhook URL we have set in App Store Connect (Sandbox) is: https://9c0f-182-79-123-254.ngrok-free.app/ios/webhook Despite successfully completing a subscription purchase in the sandbox, there is no evidence that the webhook is being triggered. We would appreciate your guidance in resolving this issue or confirming if there are any additional configurations or steps required on our end.
3
1
188
May ’25
iOS promotional code eligibility
I am implementing promotional codes for auto-renewing subscriptions in my app. I need to create the codes and have them link to a discounted auto-renewing subscription. So my normal 1 month subscription is $24.99, I would like to offer users (new subscribers and existing/expired subscribers) the ability to enter a promo code that discounts the price of the first month to $4.99, then $24.99 after the initial first month. All users, regardless of whether or not they have has a previous subscription or not should be able to use this code for this discount. From what I am seeing in documentation, promo codes are only available to users that have had a subscription previously? Currently I am receiving the response "Offer Not Available" with the error: <SKPaymentQueue: 0x2815f0d60>: Payment completed with error: Error Domain=ASDServerErrorDomain Code=3904 "Offer Not Available" UserInfo={NSLocalizedDescription=Offer Not Available I simply do NOT understand why apple engineers have to make something that should be somewhat straightforward, be SO COMPLEX, then when you try to figure out whats going wrong, no meaningful error to troubleshoot. WTF I have tried this on users that have had a previous subscription and still the same error.
3
0
500
Jan ’25
Sandbox user can't see StoreKit subscriptions
Hi everyone, I’m struggling to get StoreKit 2 to fetch products in my SwiftUI app while using a sandbox user. I think I’ve followed all necessary setup steps in Xcode, App Store Connect, and my physical test device, but Product.products(for:) always returns an empty array. I’d appreciate any insights! What I’ve Done Local App Setup (Xcode 16.2) Created a blank SwiftUI Xcode project. Enabled In-App Purchase capability under Signing & Capabilities. Implemented minimal StoreKit 2 code to fetch available products (see below). Using the correct bundle identifier, which matches App Store Connect. App Store Connect Configuration Registered the app with the same bundle identifier. Created an Auto-Renewable Subscription with: Product ID: v1 (matches my code). All fields filled (pricing, localization, etc.). Status: Ready for Review. Linked the subscription to the latest app version in App Store Connect. Sandbox User & Testing Setup Created a sandbox tester account. Logged in with the sandbox user under Settings → Developer → Sandbox Apple ID. This was on my physical device (iOS 18.2). Installed and ran the app directly from Xcode (⌘+R). Issue: StoreKit Returns No Products Product.products(for:) does not return any products. There are no errors thrown, just an empty array. I confirmed that StoreKit Configuration is set to None in Xcode. No StoreKit-related logs appear in the Console. Code Snippets //StoreKitManager.swift import StoreKit import SwiftUI @MainActor class StoreKitManager: ObservableObject { @Published var products: [Product] = [] @Published var errorMessage: String? func fetchProducts() async { do { let productIDs: Set<String> = ["v1"] // Matches App Store Connect let fetchedProducts = try await Product.products(for: productIDs) print(fetchedProducts) // Debug output DispatchQueue.main.async { self.products = fetchedProducts } } catch { DispatchQueue.main.async { self.errorMessage = "Failed to fetch products: \(error.localizedDescription)" } } } } //ContentView.swift import SwiftUI struct ContentView: View { @StateObject private var storeKitManager = StoreKitManager() var body: some View { VStack { if let errorMessage = storeKitManager.errorMessage { Text(errorMessage).foregroundColor(.red) } else if storeKitManager.products.isEmpty { Text("No products available") } else { List(storeKitManager.products, id: \.id) { product in VStack(alignment: .leading) { Text(product.displayName).font(.headline) Text(product.description).font(.subheadline) Text("\(product.price.formatted(.currency(code: product.priceFormatStyle.currencyCode ?? "USD")))") .bold() } } } Button("Fetch Products") { Task { await storeKitManager.fetchProducts() } } } .padding() .onAppear { Task { await storeKitManager.fetchProducts() } } } } #Preview { ContentView() } Additional Information iOS Version: 18.2 Xcode Version: 16.2 macOS Version: 15.3.1 Device: Physical iPhone (not simulator) TestFlight Build: Not used (app is run directly from Xcode) StoreKit Configuration: Set to None
2
0
398
Mar ’25
DID_FAIL_TO_RENEW Notification with a null gracePeriodExpiresDate
We are seeking clarification on the behavior of App Store Server Notifications V2. Summary In our production environment, we received a notification with notificationType: DID_FAIL_TO_RENEW and subtype: GRACE_PERIOD. However, the gracePeriodExpiresDate field in the payload was null. We understand this notification indicates that a user's subscription has entered a grace period. The null value for its expiration date is unexpected, and we are looking for an official explanation of this behavior and the correct way to handle it. The Scenario Here are the details of the notification we received: Notification Type: DID_FAIL_TO_RENEW Notification Subtype: GRACE_PERIOD Environment: Production Upon decoding the signedRenewalInfo JWS from the responseBodyV2, we found that the gracePeriodExpiresDate field inside the JWSRenewalInfoDecodedPayload was null. The notificationUUID for this event was in the format xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx. Our Implementation and its Impact Our backend is designed to ensure service continuity during a grace period, as recommended in the documentation. Current Logic: Receive the DID_FAIL_TO_RENEW / GRACE_PERIOD notification. Extract the gracePeriodExpiresDate. Extend the user's subscription expiration date in our database to match this date. Because the gracePeriodExpiresDate was null in this case, our logic failed, creating a risk of service interruption for the user. Context and Investigation We have performed the following checks: App Store Connect Settings: We have confirmed that Billing Grace Period is enabled for the relevant subscription group. Sandbox Environment: We have been unable to reproduce this scenario in the Sandbox. User Context: We believe the user in this case was experiencing a failed payment when attempting to renew for the first time after a free trial period. Questions To ensure we handle this scenario correctly, we would appreciate clarification on the following points: Conditions for Null: Under what specific conditions does a DID_FAIL_TO_RENEW notification with a GRACE_PERIOD subtype contain a null gracePeriodExpiresDate? Expected Behavior: Is this null value an expected behavior for certain scenarios, such as the first failed renewal after a free trial? Best Practice: If this is an expected behavior, what is the correct way to handle it? How should our backend interpret a null gracePeriodExpiresDate to ensure service continuity for the user?
2
2
180
Jul ’25
In iOS 26 beta3 version, the finishTransaction method is unable to remove transactions from the SKPaymentQueue, causing transactions to remain in the queue even after being processed.
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.
2
2
467
Jul ’25
Issue with "Transaction.currentEntitlements": Some paid users cannot use the features of our subscription plan
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.
2
3
688
Jul ’25
Revoked Non-Consumable Purchases & currentEntitlements
We use Transaction.currentEntitlements in StokeKit 2 to unlock functionality based on a Non-Consumable IAP but we have a case involving a refund that seems wrong and I am trying to understand the interation between transactionId, originalTransactionId & revocationReason. The Context: We have a universal App on macOS and iOS that offers a shared Non-Consumable IAP. For this example I have named it "app.lifetime" On macOS we use StoreKit 2 and I am calling the Transaction.currentEntitlements and Transaction.all functions. On iOS we are still using StoreKit 1. This example customer: Originally purchased "app.lifetime" on 2024-10-27 Was refunded by Apple for "app.lifetime" on 2024-10-29 Re-purchased "app.lifetime on 2025-02-24 (I have seen an email receipt of this transaction but it never shows up in Transaction data) (all the above happened on the mac via StoreKit 2) The Transactions (all lightly redacted for privacy): on macOS the following is returned from Transaction.currentEntitlements... { "appTransactionId" : "...8123", "bundleId" : "app", "currency" : "USD", "deviceVerification" : "...", "deviceVerificationNonce" : "...", "environment" : "Production", "inAppOwnershipType" : "PURCHASED", "originalPurchaseDate" : 1729997808000, "originalTransactionId" : "...9955", "price" : 1, "productId" : "app.lifetime", "purchaseDate" : 1729997808000, "quantity" : 1, "signedDate" : 1740416289102, "storefront" : "USA", "storefrontId" : "143441", "transactionId" : "...7511", "transactionReason" : "PURCHASE", "type" : "Non-Consumable" } Note in the above example the originalTransactionId & transactionId are different. Transaction.all however returns both transactions: [ { "appTransactionId" : "...8123", "bundleId" : "app", "currency" : "USD", "deviceVerification" : "...", "deviceVerificationNonce" : "...", "environment" : "Production", "inAppOwnershipType" : "PURCHASED", "originalPurchaseDate" : 1729997808000, "originalTransactionId" : "...9955", "price" : 1, "productId" : "app.lifetime", "purchaseDate" : 1729997808000, "quantity" : 1, "revocationDate" : 1730224102000, "revocationReason" : 0, "signedDate" : 1740415969925, "storefront" : "USA", "storefrontId" : "143441", "transactionId" : "...9955", "transactionReason" : "PURCHASE", "type" : "Non-Consumable" }, { "appTransactionId" : "...8123", "bundleId" : "app", "currency" : "USD", "deviceVerification" : "...", "deviceVerificationNonce" : "...", "environment" : "Production", "inAppOwnershipType" : "PURCHASED", "originalPurchaseDate" : 1729997808000, "originalTransactionId" : "...9955", "price" : 1, "productId" : "app.lifetime", "purchaseDate" : 1729997808000, "quantity" : 1, "signedDate" : 1740416289102, "storefront" : "USA", "storefrontId" : "143441", "transactionId" : "...7511", "transactionReason" : "PURCHASE", "type" : "Non-Consumable" } ] Note here that the original transaction ("...9955") includes a revocationDate and revocationReason that match the expected refund but the secondary transaction that seems to match on all other details is missing the revocation info. Looking at the iOS SK1 receipt data to compare, after a receipt refresh I see only a single transaction "...9955" which includes the cancellation info and transaction "...7511" is not present at all. The impact of this is that on iOS we are considering the purchase void but on macOS we are following currentEntitlements and consdering it still valid. Calling the inApps/v1/history/... server API with the "...7511" transactionId that is shown in the currentEntitlements response returns the "...9955" transaction with the correct revocation status but "...7511" is no returned at all. To Summarise: currentEntitlements on macOS shows transaction "...7511" as active and with an originalTransactionId of "...9955" all on macOS includes both "...7511" as active and "...9955" as revoked iOS reciept data shows only "...9955" as revoked Server API shows only "...9955" as revoked event when explicitly called with "...7511" Neither of them show a more recent purchase the same customer made for the same IAP product. My questions are: Is this a StoreKit bug or am I mis-understanding something? If it's a bug how can I work around it to ensure revoked purchases aren't still appearing in currentEntitlements? Under what conditions can StoreKit generate multiple transactionIds for the same underlying originalTransactionId? I had assumed (and the docs suggest) this only happens for subscriptions but here it is happening for a Non-Consumable IAP. Why would transactionId "...7511" only be present on macOS/SK2 and not visible at all on iOS/SK1 or API? I don't understand why the latest IAP from 2025-02-24 that the customer assures me they made (and has shown me the receipt for is not showing up in the Transactions history at all. Any ideas?
2
0
349
Mar ’25
Receiving 404 Error for APNs Server Notifications When Validating signedPayload
Hi everyone, I'm experiencing an issue with APNs server notifications where I receive a 404 error when trying to validate the signedPayload from Apple's notification. Below is a sanitized version of my code: class ServerNotificationAppleController extends Controller { // URL for StoreKit keys (Sandbox environment) private $storeKitKeysUrl = 'https://api.storekit-sandbox.itunes.apple.com/inApps/v1/keys'; public function handleNotification(Request $request) { \Log::info($request); $signedPayload = $request->input('signedPayload'); if (!$signedPayload) { return response()->json(['error' => 'signedPayload not provided'], 400); } // Step 1: Create your JWT token (token creation logic can be in a separate service) $jwtToken = $this->generateAppleJWT(); // Step 2: Send a request to the StoreKit keys endpoint $response = Http::withHeaders([ 'Authorization' => 'Bearer ' . $jwtToken, ])->get($this->storeKitKeysUrl); Log::info('Apple Keys Status:', ['status' => $response->status()]); Log::info('Apple Keys Body:', ['body' => $response->body()]); if ($response->status() !== 200) { return response()->json(['error' => "Apple public keys couldn't be retrieved"], 401); } $keysData = $response->json(); // Step 3: Validate the signedPayload $validatedPayload = $this->validateSignedPayload($signedPayload, $keysData); if (!$validatedPayload) { return response()->json(['error' => 'Invalid signedPayload'], 400); } // Process the validated data as needed Log::info("Apple Purchase Data:", (array)$validatedPayload); return response()->json(['message' => 'Notification processed successfully'], 200); } private function generateAppleJWT() { // API key details (replace placeholders with actual values) $keyId = config('services.apple.key_id'); // e.g., <YOUR_KEY_ID> $issuerId = config('services.apple.issuer_id'); // e.g., <YOUR_ISSUER_ID> $privateKey = file_get_contents(storage_path(config('services.apple.private_key'))); // Set current UTC time and expiration time (20 minutes later) $nowUtc = Carbon::now('UTC'); $expirationUtc = $nowUtc->copy()->addMinutes(20); // Create the payload with UTC timestamps $payload = [ 'iss' => $issuerId, 'iat' => $nowUtc->timestamp, 'exp' => $expirationUtc->timestamp, 'aud' => 'appstoreconnect-v1', 'bid' => 'com.example.app', // Replace with your Bundle ID if necessary ]; // Generate the JWT token return JWT::encode($payload, $privateKey, 'ES256', $keyId); } private function validateSignedPayload($signedPayload, $keysData) { try { $jwkKeys = JWK::parseKeySet($keysData); return JWT::decode($signedPayload, $jwkKeys, ['RS256']); } catch (\Exception $e) { Log::error("Apple Purchase Validation Error: " . $e->getMessage()); return null; } } } I’m particularly puzzled by the fact that I receive a 404 error when trying to retrieve the public keys from the StoreKit keys endpoint. Has anyone encountered this issue or can provide insight into what might be causing the error? Any help or suggestions would be greatly appreciated. Thanks!
2
0
403
Mar ’25
Unable to retrieve data from In App Purchase
I want to add in-app purchasing to my app, but I can't figure out what part of my workflow is wrong. I created a product for my app in iTunes Connect (the product ID is com.mycompany.products.***) and it's in "Ready to submit" status. I created a sandbox test user for this app. I connected to iTunes on a real device using the sandbox AppleID. I went back to XCode and added in-app purchasing to my app. I turned on developer mode on the real device and logged in as the sandbox user. I built the app and ran it on a real device (not the simulator). I tried to get product information (com.mycompany.products.***) but nothing was returned. In-app purchasing is registered in App Store Connect and the status is "Ready to submit". The code only retrieves product information in a simple way, so I don't think there's a problem. inAppPurchase.getProducts(["com.mycompany.products.***"]).then(console.log).catch(console.error); But it only returns an empty array. What could be wrong? Any help would be much appreciated.
2
1
71
Jul ’25
[macOS] AppTransaction questions (internet connection requirement)
Hello, I hope to find out more about how AppTransaction works on macOS, specifically about its internet connection requirements: if I use this to validate that the app is a legit purchase from the Mac App Store, I would not want it to have an always-on requirement just to validate. Does AppTransaction require the user to always be online for AppTransaction.shared ? When an app is downloaded from the Mac App Store, is the data needed for AppTransaction automatically embedded during that download, or is that data downloaded upon first launch of the app, therefore requiring an internet connection at launch time? Once the data/receipt has been downloaded by AppTransaction, is it cached until the app's next update, or is it cleared at some time during the version's life and needs to be re-downloaded, therefore requiring an internet connection at launch? Where is that receipt/data stored? Also, if you don't mind me sneaking in this non-related but sort of related question, in terms of receipt validation: Does macOS Sequoia's MAC address rotation feature affect receipt validation in any way when using IOKit? Thank you kindly, – Matthias
2
4
603
Apr ’25
Transitioning a freemium app to StoreKit 2
I'm currently working on transitioning to StoreKit 2. In order to see if my users are legacy users who purchased the app before I implemented an in-app purchase, I am trying to use the original purchase date for the app. Unfortunately, it's returning 0 seconds since 1970. func updateOriginalPurchaseStatus() async throws { let transaction = try await checkVerified(AppTransaction.shared) self.originalPurchaseVersion = transaction.originalAppVersion self.originalPurchaseDate = transaction.originalPurchaseDate } This is from the transaction: [3] = { key = "originalPurchaseDate" value = number (number = 0) } Currently trying to figure out when I actually purchased the app, but it might be as early as 2012. And I likely used a download code.
2
0
246
Mar ’25
unable to get subscription products macos iap xcode swift
Hi Apple Support, I am encountering an issue while testing in-app purchases in the sandbox environment. I have created a sandbox tester account Logged out of the App Store and System Settings on my Mac. My main developer account is signed in under Sign In & Capabilities in Xcode. The Bundle ID matches the one configured in App Store Connect. The Product ID I am querying also matches the configuration. Deleting the app and reinstalling. Restarting my Mac. When running my code in debug mode, I observe the following: Running debug build App Store environment: Production [1b294b55] Error updating Storefront: Error Domain=StoreKit_Shared.StoreKitInternalError Code=7 "(null)" Valid products: [] Invalid product IDs: ["com.x.x.x.monthly"] No products found The Product ID (com.x.x.x.monthly) matches the one I have configured in App Store Connect. The bundle id matches. When I create a StoreKit Configuration file in Xcode and sync it with my app, I can see the product IDs correctly. Below are the relevant code snippets for fetching and handling products: func fetchProducts() { guard !productIDs.isEmpty else { print("No product IDs configured") return } let request = SKProductsRequest(productIdentifiers: productIDs) request.delegate = self print("Starting product request...") request.start() } func productsRequest(_ request: SKProductsRequest, didReceive response: SKProductsResponse) { DispatchQueue.main.async { print("Valid products: \(response.products)") print("Invalid product IDs: \(response.invalidProductIdentifiers)") self.products = response.products if self.products.isEmpty { print("No products found") } else { print("products not empty") for product in self.products { print("Fetched product: \(product.localizedTitle) - \(product.priceLocale.currencySymbol ?? "")\(product.price)") } } } } func debugStoreSetup() { if let receiptURL = Bundle.main.appStoreReceiptURL { if receiptURL.lastPathComponent == "sandboxReceipt" { print("App Store environment: Sandbox") } else { print("App Store environment: Production") } } else { print("No receipt found") } } Could you help identify why my app is not recognizing the Product ID in the sandbox environment? Thank you for your assistance.
2
3
882
Jan ’25