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?
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 trying to implement in-app purchases in Apple TV.
I added a "non-consumable" product and started testing in Sandbox, but it did not work properly.
While I am trying to fetch the product from the appstore, it won't give any responses like success or failure.
So that our app gets rejected in the App Store.
Please provide me the steps to implement in-app purhcase in Apple tvos using Swift.
Note: The same code is working fine in iOS.
Hello,
Having bad times with Development Postback copies receival on our custom server.
Current setup:
App is configured to be advertised (https://developer.apple.com/documentation/adattributionkit/configuring-an-advertised-app)
AdAttributionKit - Opt in for Reengagement Postback Copies ✅
AdAttributionKit - Postback Copy URL ✅
AdAttributionKit - Ad Network Identifiers ✅
Configured backend https://{name}.com/.well-known/appattribution/report-attribution/ (POST) ✅
Devices with iOS 18.4 (with Postaback Development tool and AdAttribution developer mode Enabled)
Tried different Postback setup combinations, with different app builds (debug, release installed from xcode/testflight) and with AdAttribution developer mode Enabled/Disabled - doesn't make any difference.
Console log:
Found 0 postbacks eligible for transmission for environments:
Any advise is very much appreciated
Hello all!
My application written with C++ and using StoreKit1. For now my application using bridge C++/Objective-CPP/Objective-C/Swift. Since StoreKit1 declared "deprecated" got a question of how to implement correctly StoreKit2 in C++ application, mostly everything about Renewable Subscriptions. StoreKit2 have no direct API in Objective-C, it's available only in Swift. Which is the way the best and correct way:
-- for on-device writing C library and in it implement Swift? In this case how to add correctly StoreKit2 into library itself?
-- for on-device somehow write kind of wrapper for Swift StoreKit 2 within translation of all of objects in Objective-C and implement it with C++/Objective-CPP bridge? In this case how to add correctly StoreKit2 in C++ project written with CMake? Here https://github.com/compnerd/swift-cmake-examples/tree/main the one of the examples of interoperability C++/Swift with CMake. But how to use exactly with StoreKit2?
-- only using server side solution?
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
I'm receiving the following error when attempting to validate an in‑app purchase receipt:
Certificate verification failed at depth 0 : forge.pki.UnknownCertificateAuthority
Certificate chain validation failed: Certificate is not trusted.
This error occurs during the certificate chain validation process of the receipt's PKCS#7 container. My implementation uses node‑forge to decode the receipt, extract the embedded certificate chain, and verify that the chain properly links from the leaf certificate (which directly signed the receipt) through the intermediate certificate to the trusted Apple Inc. Root certificate.
What the Error Indicates:
"UnknownCertificateAuthority" at depth 0:
This suggests that the leaf certificate in the receipt is not being recognized as part of a valid chain because it cannot be linked back to a trusted root in my CA store.
"Certificate chain validation failed: Certificate is not trusted":
This means that the entire certificate chain does not chain up to a trusted certificate authority (in this case, the Apple Inc. Root certificate) as expected.
Steps Taken:
I verified that the receipt is a valid PKCS#7 container.
I extracted the certificate chain from the receipt. However, the receipt only provided the leaf certificate.
I manually added the intermediate certificate (AppleWWDRCAG5.pem) to complete the chain.
I loaded the official Apple Inc. Root certificate (AppleIncRootCertificate.pem) into my CA store.
Despite these steps, the validation still fails at depth 0, indicating that the leaf certificate is not recognized as being issued by a trusted authority.
Request for Assistance:
Could you please help clarify the following points:
Is the certificate chain for receipts (leaf → intermediate → Apple Inc. Root) as expected, or has there been any change in the chain that I should account for?
Is there a recommended or updated intermediate certificate I should be using for receipt validation?
Are there known issues or recent changes on Apple's side that might cause the leaf certificate to not be recognized as part of a valid chain?
Any guidance to resolve this certificate chain validation error would be greatly appreciated.
Hi,
I'm a server developer working on an iOS app, and I've encountered an interesting case that I'd like to understand better.
We received a user report indicating that they were able to successfully start a subscription without any charge being processed on their payment method. The subscription appears to be active in our system and we can verify the receipt with Apple's server, but the user claims no payment was deducted from their account.
I'm curious to know:
Are there any known scenarios where a subscription might activate without an immediate payment charge?
What would be the recommended way to handle such cases from the developer's perspective?
Could this be related to any specific subscription states or edge cases in the IAP system?
Any insights or documentation references would be greatly appreciated. I want to ensure we're handling our subscription logic correctly and providing the best experience for our users.
Thank you in advance for your help!
有人遇到这个问题吗,在支付的时候提示未知错误,具体的错误信息如下:
交易失败,outTradeNo:2025022631999900326, productId:com.f6car.p0001, error:Err-or -Domain=SKErrorDomain Code=0 "发生未知错误" UserInfo={NSLocalizedDescription=发生未知错误, NSUnderlyingError=0x302f50120 {Error Domain=ASDServerErrorDomain Code=3512 "无效的应用程序外部版本。" UserInfo={NSLocalizedFailureReason=无效的应用程序外部版本。}}}
寻求解决方案,感谢.
Does anyone know why I set the offer code in Apple Store Connect, but when I call SKPaymentQueue.default().presentCodeRedemptionSheet() in the app, the app icon does not appear and I cannot find the offer code?
THX
I ran into a problem. When using Storekit1 to purchase an SKU, the user payment was successful, but StoreKit1 did return paymentCancelled to my App. I would like to know under what circumstances this problem may occur? How do I fix it? Thank you
users download app with Streamlined Purchasing ,but the logic of checking subscription doesn't work. there the code:
func checkSubscriptionStatus() async {
for await entitlement in Transaction.currentEntitlements {
guard case .verified(let transaction) = entitlement else { continue }
if transaction.productID == monthlyProductID || transaction.productID == yearlyProductID {
if transaction.revocationDate == nil && !transaction.isUpgraded {
let activeSubscribed = transaction.expirationDate ?? .distantFuture > .now
if activeSubscribed {
hasActiveSubscription = activeSubscribed
// other operation
}
}
}
}
}
A customer of mine signed up for a free trial. I got a apple server notification with notification type DID_RENEW. What does that mean? Does that mean that they will be charged the subscription price now?
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?
Dear Apple Support,
We are currently in the process of migrating from Apple Server Notifications v1 to v2. As you know, once we switch from v1 to v2 in App Store Connect, this change is irreversible. Our team needs to ensure that our backend environment, developed in Python with Django and using the app-store-server-library==1.5.0, is fully prepared to handle all possible notification types before making that final switch.
While we can receive a basic test notification from Apple, it doesn’t provide the range of data or scenarios (e.g., INITIAL_BUY, RENEWAL, GRACE_PERIOD, etc.) that our code must be able to process, store in our database, and respond to accordingly. Without comprehensive, example-rich raw notification data, we are left guessing the structure and full scope of incoming v2 notifications.
Could you please provide us with a set of raw, fully representative notification payloads for every notification type supported in v2? With these examples, we can simulate the entire lifecycle of subscription events, verify our logic, and confidently complete our migration.
Thank you very much for your assistance.
I have an App Clip that uses SKOverlay.AppClipConfiguration to install the full app. Before I added a Live Activity call (Activity.request), the user could see “Install,” then “Open.” Now, once “Get” is tapped, the Clip immediately closes—no “Open” button appears. If I remove the Live Activity code, it works again.
I’ve confirmed that parent/child entitlements match, and tested via TestFlight. Is there a known issue or recommended workaround for combining SKOverlay + Live Activities in an App Clip so it doesn’t dismiss prematurely? Any insights are appreciated!
Note live activity is for App Clip only.
User Initiated a Single Consumable Purchase but Was Charged Twice
A user initiated a single in-app purchase for a consumable item, but they were charged twice. Both transactions have the same purchase token.
Additional details: After the user successfully completed the in-app purchase, the completeTransactions callback was triggered again. This was called at app launch using SwiftyStoreKit.completeTransactions to finish any pending transactions.
Could this be causing the duplicate charge? Any insights would be appreciated.
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!
Hi,
I have developed an app which has two in-app purchase subscriptions. During the test, the app can successfully get the status of the subscriptions. After it's released, I downloaded it from app store and subscribed it with my apple account. I found that in most cases, the app can identify that I have subscribed it and I can use its all functions. But yesterday, when I launched it again, it showed the warning that I haven't subscribed it. I checked my subscription in my account and the subscription status hasn't been changed, that is, I have subscribed it. And after one hour, I launched it again. This time the app identified that I have subscribed it. Why? The following is the code about listening to the subscription status. Is there any wrong about it?
HomeView()
.onAppear(){
Task {
await getSubscriptionStatus()
}
}
func getSubscriptionStatus() async {
var storeProducts = [Product]()
do {
let productIds = ["6740017137","6740017138"]
storeProducts = try await Product.products(for: productIds)
} catch {
print("Failed product request: \(error)")
}
guard let subscription1 = storeProducts.first?.subscription else {
// Not a subscription
return
}
do {
let statuses = try await subscription1.status
for status in statuses {
let info = try checkVerified(status.renewalInfo)
switch status.state {
case .subscribed:
if info.willAutoRenew {
purchaseStatus1 = true
debugPrint("getSubscriptionStatus user subscription is active.")
} else {
purchaseStatus1 = false
debugPrint("getSubscriptionStatus user subscription is expiring.")
}
case .inBillingRetryPeriod:
debugPrint("getSubscriptionStatus user subscription is in billing retry period.")
purchaseStatus1 = false
case .inGracePeriod:
debugPrint("getSubscriptionStatus user subscription is in grace period.")
purchaseStatus1 = false
case .expired:
debugPrint("getSubscriptionStatus user subscription is expired.")
purchaseStatus1 = false
case .revoked:
debugPrint("getSubscriptionStatus user subscription was revoked.")
purchaseStatus1 = false
default:
fatalError("getSubscriptionStatus WARNING STATE NOT CONSIDERED.")
}
}
} catch {
// do nothing
}
guard let subscription2 = storeProducts.last?.subscription else {
// Not a subscription
return
}
do {
let statuses = try await subscription2.status
for status in statuses {
let info = try checkVerified(status.renewalInfo)
switch status.state {
case .subscribed:
if info.willAutoRenew {
purchaseStatus2 = true
debugPrint("getSubscriptionStatus user subscription is active.")
} else {
purchaseStatus2 = false
debugPrint("getSubscriptionStatus user subscription is expiring.")
}
case .inBillingRetryPeriod:
debugPrint("getSubscriptionStatus user subscription is in billing retry period.")
purchaseStatus2 = false
case .inGracePeriod:
debugPrint("getSubscriptionStatus user subscription is in grace period.")
purchaseStatus2 = false
case .expired:
debugPrint("getSubscriptionStatus user subscription is expired.")
purchaseStatus2 = false
case .revoked:
debugPrint("getSubscriptionStatus user subscription was revoked.")
purchaseStatus2 = false
default:
fatalError("getSubscriptionStatus WARNING STATE NOT CONSIDERED.")
}
}
} catch {
// do nothing
}
if purchaseStatus1 == true || purchaseStatus2 == true {
purchaseStatus = true
} else if purchaseStatus1 == false && purchaseStatus2 == false {
purchaseStatus = false
}
return
}
Topic:
App & System Services
SubTopic:
StoreKit
I am looking to move from paid app to fremium without upsetting my existing users. I see WWDC2022 session where new fields introduced in iOS 16 are used to extract original application version user used to purchase the app. While my app supports iOS 14 and above, I am willing to sacrifice iOS 14 and go iOS 15 and above as StoreKit2 requires iOS 15 at the minimum. The code below is however only valid for iOS 16. I need to know what is the best way out for iOS 15 devices if I am using StoreKit2? If it is not possible in StoreKit2, then how much is the work involved in original StoreKit API(because in that case I can handle for iOS 14 as well)?
In the sandbox environment, when I quickly and repeatedly purchase an item, Transaction.id will be repeated.
Will there be duplication in the production environment?
func pay(productId: String, orderId: String) async {
guard !productId.isEmpty, !orderId.isEmpty else { return }
let orderObj = ApplePayOrderModel.init(orderId: orderId, productId: productId)
do {
let result = try await Product.products(for: [productId])
guard let product = result.first else {
return
}
let purchaseResult = try await product.purchase()
switch purchaseResult {
case .success(let verification):
switch verification {
case .verified(let transaction):
orderObj.transactionId = String(transaction.id)
await transaction.finish()
case .unverified(let transaction, let error):
await transaction.finish()
}
case .userCancelled:
break
case .pending:
break
@unknown default:
break
}
} catch {
print("error \(error.localizedDescription)")
}
}
Topic:
App & System Services
SubTopic:
StoreKit