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

IAP Can not Use Sand Box Test
I am currently using StoreKit2 to set up the in-app purchase subscription flow, and I have already configured the subscription products in App Connect. I created a StoreKit Configuration file in Xcode and used it in the scheme. However, after completing the purchase, the transaction.jsonRepresentation data returns a transactionId of 0. After checking the documentation, I found that I need to disable the StoreKit Configuration and enable Sandbox Testing. But after disabling the StoreKit Configuration, I can't retrieve the real product data using Product.products(for: productIds). I can confirm that the ProductId I provided is real and matches the data configured in App Connect. Could you please help me identify the issue?
0
0
174
Feb ’25
In App Purchase does not work
I read the documentation and it told I had to prepare the product on App Store connect and once it is at the state "Ready to submit" I could access it on a phone where I am connected with an Icloud account in the developper list of the apple development account. This is what I've done but when I try to fetch in my flutter code the product with the id I set in App Store connect it says "No product found" Here is where I fetch the product: Future purchaseProduct(String productId) async { try { Set<String> _pIds = {productId}; final ProductDetailsResponse response = await _iap.queryProductDetails(_pIds); if (response.productDetails.isEmpty) { throw 'Product not found'; } final ProductDetails productDetails = response.productDetails.first; final PurchaseParam purchaseParam = PurchaseParam(productDetails: productDetails); _iap.buyConsumable(purchaseParam: purchaseParam); } catch (e) { Services.debugLog('Error purchasing product: $e'); throw e; } } I checked the product ID and it does not seems to be the problem. Is there some other steps I need to do ?
2
0
367
Feb ’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
tvOS In-app purchase not working
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.
1
0
304
Feb ’25
Behavior of the "get all subscription statuses" API.
We are running auto-renewing subscriptions with StoreKit2 and the “get all subscription statuses” API is behaving unexpectedly. record the originalTransactionId from the iPhone to the server side when purchasing a subscription with Storekit2. query the get all subscription statuses API from the server side with the originalTransactionId recorded. get all subscription statuses returns a response, but there is no data in the response that matches the originalTransactionId. I have an error on my system because I have built my system on the assumption that all subscriptions including originalTransactionId will be returned.
1
2
438
Feb ’25
Operation of Server Notifications V2 when Apple account is withdrawal
Please allow me to confirm the Server Notifications V2 specification. I am aware that if withdrawal an Apple account that has a subscription, the subscription will eventually be cancelled. Regarding Server Notifications V2 notifications with a notificationType of EXPIRED, am I correct in thinking that they will be sent when the subscription expires even if the Apple account is withdrawal?
0
0
290
Feb ’25
App Store Server Notification sends old transactions
I've noticed that CONSUMPTION_REQUEST notifications sometimes have a signedTransactionInfo which corresponds not to the latest transaction, but to an earlier transaction in a subscription. Is this expected? I thought signedTransactionInfo was always the latest subscription information? Are there any other notification types for which signedTransactionInfo can be out of date?
2
0
359
Feb ’25
Renewal Info never contains offerType
Signed renewal info from 'Get Subscription Statuses' or in server notifications never has the offerType or offerDiscountType even when the corresponding transaction does have those values set. Our offer is a free trial. Do these properties refer to something different in JWSRenewalInfoDecodedPayload than they do in transactions? I'm trying to determine whether a subscription (identified by originalTransactionId) is currently in a free trial based on server notifications. The status doesn't tell us if the subscription is currently in free trial and the signedTransactionInfo may be for an older transaction.
1
0
651
Feb ’25
Is wallet extension an mandatory implementation for Apple Pay and Add to wallet?
I am working on a banking application (includes iPhone and iPad) which includes add to wallet feature. During the implementation I saw one document it is mentioned that for iPad app, the app must be extended to support Apple pay functionality. Details from document says "Card Issuers with an iOS mobile banking app must support Card Issuer iOS Wallet extension functionality to enable Card issuer mobile app customers to provision new cards directly from the iOS Wallet app with all eligible Apple iOS devices. If the Card Issuer has a dedicated iPad App, that App must be extended to support Apple Pay functionality. " Is wallet extension implementation required for Apple pay to work in iPhone and iPad? Is wallet extension a mandatory implementation for Add to Apple Wallet feature to work and approved by Apple? I am little confused in this. Anyone who integrated Apple pay or done add to Apple Wallet feature recently without wallet extension faced any rejection? Any help would be much appreciated.
4
2
3.7k
Feb ’25
ASDServerErrorDomain Code=3512
有人遇到这个问题吗,在支付的时候提示未知错误,具体的错误信息如下: 交易失败,outTradeNo:2025022631999900326, productId:com.f6car.p0001, error:Err-or -Domain=SKErrorDomain Code=0 "发生未知错误" UserInfo={NSLocalizedDescription=发生未知错误, NSUnderlyingError=0x302f50120 {Error Domain=ASDServerErrorDomain Code=3512 "无效的应用程序外部版本。" UserInfo={NSLocalizedFailureReason=无效的应用程序外部版本。}}} 寻求解决方案,感谢.
0
0
245
Feb ’25
How to validate Streamlined Purchasing with storekit 2
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 } } } } }
0
0
249
Mar ’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
App Clip Closes Before SKOverlay Can Show “Open” Button When Live Activity Is Involved
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.
1
0
281
Mar ’25
Duplicate In-App Order with two Different Transaction ID
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.
0
0
221
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
402
Mar ’25
Why can't the app get the subscription status occasionally?
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 }
0
0
205
Mar ’25