I'm opening this post because I've encountered a perplexing issue in my application utilizing StoreKit 2 with the Sandbox environment for subscription validation.
My app makes a server call each time it opens to verify if there's an active subscription.
The problem arose after successfully making a purchase in the Sandbox. When I clear history from the Sandbox user and reopen the app, it resends a request to check the subscription, indicating that the user is still subscribed even though the purchases were deleted. Has anyone encountered a similar issue?
if I testing it with transaction manager in Xcode it working well.
`
func updatePurchasedProducts() async {
for await result in Transaction.currentEntitlements {
guard case .verified(let transaction) = result else {
continue
}
if transaction.revocationDate == nil {
self.purchasedProductIDs.insert(transaction.productID)
print("# purchased")
} else {
self.purchasedProductIDs.remove(transaction.productID)
print("# canceled")
}
}
}
Thank you very much!
StoreKit
RSS for tagSupport in-app purchases and interactions with the App Store using StoreKit.
Posts under StoreKit tag
200 Posts
Sort by:
Post
Replies
Boosts
Views
Activity
I am looking at StoreKit Views, new in iOS 17.
In a SubscriptionStoreView, is it possible to show the user their current subscription's renewal/expiry date, or to present a cancel button?
According to https://developer.apple.com/documentation/storekit/storebuttonkind/4203108-cancellation , storeButton for: .cancellation is "A type of button for canceling a subscription.", but actually it just shows a (X) button at the top right to dismiss the view - and this is what the WWDC video presenter (2023 session 10013) seems to believe it should do (around 28:30, "The cancellation button shows a platform-appropriate button to dismiss the view"). Is that a documentation bug?
It seems that I can show renewal/expiry dates and a cancellation button if I use AppStore.showManageSubscriptions(...). But this is rather disjointed. Shouldn't I be able to show all of this in one place? Does my UI need two separate buttons for these overlapping features? Or am I missing something?
I have an app that I distribute to beta testers via TestFlight. The app has auto-renewable subscriptions. In the sandbox environment the subscriptions expire at an accelerated rate; every hour. They renew up to 12 times. This means that my testers have to re-subscribe every 12 hours.
Is there a way to make the sandbox operate at realtime for certain users?
This is re-posted from this Stack Overflow post.
I am looking at validating the purchase of a paid app from Mac AppStore. Based on this WWDC video about StoreKit 2, I am attempting to this with AppTransaction. I have not found meaningful high-level documentation about this specific use case beyond that.
My approach is to first get the "cached" AppTransaction by calling AppTransaction.shared. If that is not there I proceed to getting it from Apple, via AppTransaction.refresh(). If they don't have it, or when the network is down, the user automagically gets the familiar "log in to your store account" UI that has been around as long as the Mac AppStore.
Once I have the AppTransaction I use it to verify we are on the right device, using code like this, where the returned Bool represents validation success:
guard let deviceVID = AppStore.deviceVerificationID?.uuidString.lowercased() else { return false }
let nonce = appTransaction.deviceVerificationNonce.uuidString.lowercased()
let combo = nonce + deviceVID
let digest = SHA384.hash(data: Data(combo.utf8))
return (digest == appTransaction.deviceVerification)
My first question is: Does that look like the right approach? Is there something else I should do, or check?
My second question is around testing this approach. Refreshing the AppTransaction in the sandbox invariably yields a valid item, even if the app version does not yet exist in AppStoreConnect. This is also the case when I log out in the App Store app on the Mac. This makes me think it is using my AppleID which I am logged into in System Settings. Does that sound right?
I would like to be able to remove / delete the cached AppTransactions - where might I find those on the system?
Thanks for everyone's help!
I've seen several posts (mostly from 3+ years ago) asking about implementing a "home-rolled" free trial for subscriptions in iOS. I'm still a bit confused though as there doesn't seem to be a definitive answer.
I'm wanting to try to increase registration -> free trial conversion by offering users a time-based free trial from my backend rather than going through StoreKit. It will be clear up front that it's a subscription service and has a cost, but they won't need to agree to the auto payment after X days.
Is anyone currently doing this without getting rejected or is this still considered a no-no by Apple?
How can I add listening for transactions which use a an offer code to purchase an auto renewable subscription. I implemented listening for transactions using the example code included in the WWDC21-10114 session. Code works when user purchases a subscription, but not when they use the promo code, then purchase the discounted subscription.
I have implemented auto renewable subscriptions in my app, as well as promo codes. Purchase of subscriptions both monthly and annual; work correctly. What I don't know is what to "listen for" instead of product, when the user uses a promo code to purchase the product. am I looking for a different product code? or product identifier when the offer code is used to subscribe?
The iOS App Sandbox plays a pivotal role in ensuring the secure testing of In-App Purchases (IAPs) without impacting real users. To utilize the sandbox, set up your app's IAPs in App Store Connect. Begin by importing the StoreKit framework and fetching product information using SKProductsRequest. Once products are retrieved, initiate purchases through SKPaymentQueue. Implement the SKPaymentTransactionObserver to handle transaction updates, distinguishing between successful, failed, and restored purchases. When testing, remember to use test user accounts and sign in to the App Store with a test account on your device. This ensures a controlled environment for validating IAPs during development.
URL - https://www.controlf5.in/ios-mobile-app-development-company/
swift
Copy code
import StoreKit
class YourViewController: UIViewController, SKProductsRequestDelegate, SKPaymentTransactionObserver {
func fetchProducts() {
let productIdentifiers: Set<String> = ["com.yourapp.product1", "com.yourapp.product2"]
let request = SKProductsRequest(productIdentifiers: productIdentifiers)
request.delegate = self
request.start()
}
func productsRequest(_ request: SKProductsRequest, didReceive response: SKProductsResponse) {
let products = response.products
// Handle retrieved products
}
func request(_ request: SKRequest, didFailWithError error: Error) {
// Handle error
}
func purchaseProduct(product: SKProduct) {
let payment = SKPayment(product: product)
SKPaymentQueue.default().add(payment)
}
func paymentQueue(_ queue: SKPaymentQueue, updatedTransactions transactions: [SKPaymentTransaction]) {
for transaction in transactions {
switch transaction.transactionState {
case .purchased:
// Handle successful purchase
case .failed:
// Handle failed purchase
case .restored:
// Handle restored purchase
default:
break
}
}
}
}
Need some help here.
I've got an iPhone 11 PM on 16.6.1 and I'm trying to test Family Sharing IAPs. However, I can't seem to test via the Sandbox environment (which I need to validate receipt handling for Family Sharing). The app is running locally on my device and was built straight from Xcode.
When I tap to make a purchase in my app (which uses StoreKit2, if that makes any difference) a sheet pops up with a purchase button which, when tapped, immediately completes and I get the following dialog box:
"Your purchase was successful"
[Environment: Xcode]
How do I get my app to use the Sandbox environment? All documentation suggestions when I tap my purchase button I should simply be presented with a login modal and then, after the purchase has completed, be able to see my Sandbox credentials under Settings -> App Store. At the moment no dialog is presented (the purchase completes immediately) and the entire "Sandbox Account" section is missing from Settings -> App Store. Any help will be greatly appreciated!
I have a MacOS application that has a non-consumable in-app purchase that unlocks an enhanced version of the app (in other words, it's a one-time upgrade purchased inside of the app). The transaction is handled using StoreKit 2 and the app is distributed on the MacOS App Store.
I would like to add an affiliate-like program where certain people can promote my application and receive a percentage of the earnings from users that buy the in-app purchase as a result of their promotion. In order to correctly distribute earnings, I need some way to track that a purchase from a user is linked to a certain affiliate.
Research led me to offer codes but I quickly realised that these are only valid for subscriptions (which my purchase is not), and, besides, it seems they are not supported on MacOS. Another constraint that I have is that my application should not make any external network requests (apart from those to Apple servers, like StoreKit), so I cannot use something like Firebase for a custom offer implementation.
I'm not sure what the best way to achieve this is. One way I thought of is to create one non-consumable in-app purchase/product for each affiliate and use a deep link to associate a user with that product. Then, I'll know which affiliate each user comes from based on the product that was used during the purchase. The only problem with this is that products need to be aded at compile-time so each new affiliate I add would require me to publish a new app version.
I'm wondering whether there's a better way to do this?
SKAN / SkAdNetwork Error
ERROR: We never received any http request from the "skan.ourdomain.com" as expected.
ERROR: All the attributions(YES! ALL OF THE INSTALLS) in the Google Analytics / Singular / AppsFlyer / Twitter / Facebook, shows the source are "direct organic".
We Did These
We use Google Ads and Twitter Ads and Facebook Ads to promote our apps. We spent enough money, and got thousands of paid installs from these ads.
We set the NSAdvertisingAttributionReportEndpoint to "skan.ourdomain.com".
We call the SKAdNetwork.updatePostbackConversionValue(1). in "AppDelegate" and "Subscribe" source codes.
And from our app logs, we see there are SKAN_UPDATE_CONVERSION_VALUE_OK.
Source Code
if #available(iOS 15.4, *) {
SKAdNetwork.updatePostbackConversionValue(1) { err in
if let err = err {
Tracker.shared.reportEvent(.SKAN_UPDATE_CONVERSION_VALUE_FAIL, name: err.localizedDescription, value: 1)
} else {
Tracker.shared.reportEvent(.SKAN_UPDATE_CONVERSION_VALUE_OK)
}
}
} else {
SKAdNetwork.registerAppForAdNetworkAttribution()
Tracker.shared.reporxtEvent(
.SKAN_UPDATE_CONVERSION_VALUE_OLD_VERSION, name: "AppDelegate")
}
I'm currently testing subscriptions in Sandbox. In AppstoreConnect, I set a grace period of 3 days. I subscribed for a service which expired and now it's inBillingRetryPeriod state. I thought it had to do with my payment method. After updating my payment method, it still remains in that state. I checked Status.RenewalInfo's gracePeriodExpiration and expirationReason values but both produced nil. How do I exit the inBillingRetry state? I'm new to in-app purchases. Thanks. Here's the relevant code that updates subscription status:
@MainActor
func updateSubscriptionStatus() async {
do {
guard let product = storeManager.renewables.first,
let statuses = try await product.subscription?.status else {return}
var highestProduct: Product? = nil
var highestStatus: Product.SubscriptionInfo.Status? = nil
for status in statuses {
switch status.state {
case .expired, .revoked:
continue
default:
let verifiedRenewalInfo = try storeManager.checkVerified(status.renewalInfo)
//Find the first subscription in the store that matches id on the `status.renewalInfo`
guard let newSubscription = storeManager.renewables.first(where: {$0.id == verifiedRenewalInfo.autoRenewPreference}) else { continue }
guard let currentProduct = highestProduct else {
highestProduct = newSubscription
highestStatus = status
// next status
continue
}
let currentProductTier = storeManager.tierDuration(for: currentProduct.id)
let newTier = storeManager.tierDuration(for: newSubscription.id)
if newTier > currentProductTier {
//updated product and status
highestProduct = newSubscription
highestStatus = status
}
}
}
currentSubscription = highestProduct // currentSubscription is an @State
status = highestStatus // status is an @State
if let mySubcriptionStatus = status,
case .verified(let renewalInfo) = highestStatus?.renewalInfo {
print(mySubcriptionStatus.state) //
StoreKit.Product.SubscriptionInfo.RenewalState(rawValue: 3))-- inBillingRetry.
print(renewalInfo.expirationReason) // nil
print(renewalInfo.gracePeriodExpirationDate) // nil
}
} catch {
print(error)
}
}
I am really frustrated with storekit2, is it me or is it an Apple bug?
the subscription doesn't exist LITERALLY and the code still append a subscription to my currentSubscriptions!!
Unfortunatelly I cannot attach an image but the susbcrioption doesn't exist in the storekit debug window but the code still append a valid subscription!
https://stackoverflow.com/questions/77783897/storekit2-subscription-is-not-existant-and-still-append-subscription-bug
// update the customers products
@MainActor
func updateCustomerProductStatus() async {
var purchasedSubs: [Product] = []
var purchasedIAP: [Product] = []
//iterate through all the user's purchased products
for await result in Transaction.currentEntitlements {
do {
//again check if transaction is verified
let transaction = try checkVerified(result)
//Check the `productType` of the transaction and get the corresponding product from the store.
switch transaction.productType {
case .consumable:
if let iap = iaps.first(where: { $0.id == transaction.productID }) {
purchasedIAP.append(iap)
}
case .autoRenewable:
if let subscription = subscriptions.first(where: { $0.id == transaction.productID }) {
//SUBSCRIPTION DOESN'T EXIST AND STILL GETS APPENDED!!
purchasedSubs.append(subscription)
}
default:
break
}
} catch {
//storekit has a transaction that fails verification, don't delvier content to the user
print("Transaction failed verification")
}
//finally assign the purchased products
self.purchasedIAPs = purchasedIAP
self.purchasedSubscriptions = purchasedSubs
}
}
I have implemented Store Kit for my Swift UI App. I defined all products in app store connect (auto-renewables & non-renewables).
I tested everything in Xcode and it seems to run fine. However i want to test it in Sandbox to be able to check the server side dependencies.
After creating Sandbox users and logging in to those accounts on my physical device, i am still only able to do payments in Xcode ("[Environment: Xcode]").
I added the In-App Purchase Capability to my project in the Signing & Capability Targets and made sure the app runs in debug mode.
So according to the docs (https://developer.apple.com/documentation/storekit/in-app_purchase/testing_in-app_purchases_with_sandbox) everything seems to be set-up.
It seems that with StoreKit and using .currentEntitlementTask, there is no use for a 'restore purchases' button. Purchase information is restored automatically. But in the app review process, the reviewer thinks that there is still need for a 'restore purchase' option.
What should the 'restore purchases' action do? Is this documented for StoreKit in some place?
Hi all,
I have an app which is essentially a keyboard extension. Some features of the keyboard require a subscription to work, and this subscription can be bought in the main app.
My question is, can the keyboard extension use StoreKit 2 to verify the validity of a subscription created in the main app? If this isn't possible (due to sandboxing or whatever), I suppose I can just write status information that's shared using an app group, but the problem with this is that the app is likely to run seldomly. This mean that if the subscription is cancelled or lapses, the main app may not see it for a while, and the keyboard will continue to functions even though it has expired.
One other thing I've thought of is maybe to share the receipt of the purchase with the extension, and have the keyboard sync and verify the receipt periodically with Apple's servers. Is this the proper way to do it?
Thanks!
We had a few users reporting this issue where our app is unable to connect to StoreKit.
Failed product request from the App Store server: systemError(Error Domain=NSCocoaErrorDomain Code=4097 "connection to service named com.apple.storekitagent" UserInfo={NSDebugDescription=connection to service named com.apple.storekitagent})
This occurs when calling Product.products(for:).
Some users mentioned they had to restart their Mac in safe mode to make the error go away, some had DNS cache issues and clearing those helped, or some found the culprit to be Adguard.
What could be causing this error as it is not very clear what's causing it?
Hi, I am trying to download to my iPhone SKADNetwork profile and every time it pops us this error:
Profile Error:
Profile "SKAdNetwork Developer Testing" has an invalid signature.
We need this profile for testing. Can you please help?
In case it is relevant: my company renewed the yearly membership a few days ago and I have Developer Mode activated on my iPhone.
Thanks and have a great day!
Hello.
I have a question about migrating from Original StoreKit to StoreKit2.
In my app, there are two in-app purchase products implemented using Original StoreKit: Product A and Product B.
For each product, there are separate backend servers, and due to certain reasons, we need to migrate one of those servers to use the AppStore Server API instead of the VerifyReceipt API.
During this migration, the iOS app needs to have code that combines both Original StoreKit and StoreKit2 for each product.
I heard that combining StoreKit2 with the VerifyReceipt API is not recommended. To avoid doing so, I believe the implementation would look like the below. Is this approach problematic? Is it not recommended by Apple?
If anyone has successfully implemented a similar migration and has insights, I would appreciate hearing about it.
Code implemented using Original StoreKit
SKPaymentQueue.default().add(payment)
func paymentQueue(_ queue: SKPaymentQueue, updatedTransactions transactions: [SKPaymentTransaction]) {
for transaction in transactions.filter({ $0.payment.productIdentifier.hasPrefix("...Original StoreKit product only...")}) {
switch transaction.transactionState {
case .purchasing: break
case .deferred: print(Messages.deferred)
case .purchased: handlePurchased(transaction) // send base64 encoded receipt file.
case .failed: handleFailed(transaction)
case .restored: handleRestored(transaction)
@unknown default: fatalError(Messages.unknownPaymentTransaction)
}
}
}
Code implemented using StoreKit2
let result: Product.PurchaseResult = try await product.purchase()
switch result {
case .success(let verification):
let transaction = try checkVerified(verification)
// send transaction id
// ...
await transaction.finish()
case .pending:
break
case .userCancelled:
break
@unknown default:
break
}
func observeTransactions() -> Task<Void, Error> {
return Task(priority: .background) {
for await result in Transaction.unfinished {
do {
let transaction = try self.checkVerified(result)
guard transaction.productID.hasPrefix("... StoreKit2 product only...") else {
return
}
// send transaction id
// ...
await transaction.finish()
} catch {
// ...
}
}
}
}
We want to know whether the refund requested by the user for the consumable IAP of Apple is refunded fully or partially. I can get the revocation date on when the refund was processed but I also want to know whether the user got a refund fully or partially and its amount as well if possible.
we tried to get transaction info and also the refund history of App Store Server API but we are only getting the revocation date and revocation reason we also want to know if the refund was processed as fully or partially and how much money did the user got back on refund successful. Also checked the webhook data we get for REFUND notificationType, we don't get back any field that helps us identify whether refund was full or partial and its amount as well.