Hello, I’m trying to change the business model of my app to in-app subscriptions. My goal is to ensure that previous users who paid for the app have access to all premium content seamlessly, without even noticing any changes.
I’ve tried using RevenueCat for this, but I’m not entirely sure it’s working as expected. I would like to use RevenueCat to manage subscriptions, so I’m attempting a hybrid model. On the first launch of the updated app, the plan is to validate the app receipts, extract the originalAppVersion, and store it in a variable. If the original version is lower than the latest paid version, the isPremium variable is set to true, and this status propagates throughout the app. For users with versions equal to or higher than the latest paid version, RevenueCat will handle the subscription status—checking if a subscription is active and determining whether to display the paywall for premium features.
In a sandbox environment, it seems to work fine, but I’ve often encountered situations where the receipt doesn’t exist. I haven’t found a way to test this behavior properly in production. For example, I uploaded the app to TestFlight, but it doesn’t validate the actual transaction for a previously purchased version of the app. Correct me if I’m wrong, but it seems TestFlight doesn’t confirm whether I installed or purchased a paid version of the app.
I need to be 100% sure that users who previously paid for the app won’t face any issues with this migration. Is there any method to verify this behavior in a production-like scenario that I might not be aware of?
I’m sharing the code here to see if you can confirm that it will work as intended or suggest any necessary adjustments.
func fetchAppReceipt(completion: @escaping (Bool) -> Void) {
// Check if the receipt URL exists
guard let receiptURL = Bundle.main.appStoreReceiptURL else {
print("Receipt URL not found.")
requestReceiptRefresh(completion: completion)
return
}
// Check if the receipt file exists at the given path
if !FileManager.default.fileExists(atPath: receiptURL.path) {
print("The receipt does not exist at the specified location. Attempting to fetch a new receipt...")
requestReceiptRefresh(completion: completion)
return
}
do {
// Read the receipt data from the file
let receiptData = try Data(contentsOf: receiptURL)
let receiptString = receiptData.base64EncodedString()
print("Receipt found and encoded in base64: \(receiptString.prefix(50))...")
completion(true)
} catch {
// Handle errors while reading the receipt
print("Error reading the receipt: \(error.localizedDescription). Attempting to fetch a new receipt...")
requestReceiptRefresh(completion: completion)
}
}
func validateAppReceipt(completion: @escaping (Bool) -> Void) {
print("Starting receipt validation...")
guard let receiptURL = Bundle.main.appStoreReceiptURL else {
print("Receipt not found on the device.")
requestReceiptRefresh(completion: completion)
completion(false)
return
}
print("Receipt found at URL: \(receiptURL.absoluteString)")
do {
let receiptData = try Data(contentsOf: receiptURL, options: .alwaysMapped)
print(receiptData)
let receiptString = receiptData.base64EncodedString(options: [])
print("Receipt encoded in base64: \(receiptString.prefix(50))...")
let request = [
"receipt-data": receiptString,
"password": "c8bc9070bf174a8a8df108ef6b8d2ae3" // Shared Secret
]
print("Request prepared for Apple's validation server.")
guard let url = URL(string: "https://buy.itunes.apple.com/verifyReceipt") else {
print("Error: Invalid URL for Apple's validation server.")
completion(false)
return
}
print("Validation URL: \(url.absoluteString)")
var urlRequest = URLRequest(url: url)
urlRequest.httpMethod = "POST"
urlRequest.httpBody = try? JSONSerialization.data(withJSONObject: request)
URLSession.shared.dataTask(with: urlRequest) { data, response, error in
if let error = error {
print("Error sending the request: \(error.localizedDescription)")
completion(false)
return
}
guard let data = data else {
print("No response received from Apple's server.")
completion(false)
return
}
print("Response received from Apple's server.")
do {
if let json = try JSONSerialization.jsonObject(with: data) as? [String: Any] {
print("Response JSON: \(json)")
// Verify original_application_version
if let receipt = json["receipt"] as? [String: Any],
let appVersion = receipt["original_application_version"] as? String {
print("Original application version found: \(appVersion)")
// Save the version in @AppStorage
savedOriginalVersion = appVersion
print("Original version saved in AppStorage: \(appVersion)")
if let appVersionNumber = Double(appVersion), appVersionNumber < 1.62 {
print("Original version is less than 1.62. User considered premium.")
isFirstLaunch = true
completion(true)
} else {
print("Original version is not less than 1.62. User is not premium.")
completion(false)
}
} else {
print("Could not find the original application version in the receipt.")
completion(false)
}
} else {
print("Error parsing the response JSON.")
completion(false)
}
} catch {
print("Error processing the JSON response: \(error.localizedDescription)")
completion(false)
}
}.resume()
} catch {
print("Error reading the receipt: \(error.localizedDescription)")
requestReceiptRefresh(completion: completion)
completion(false)
}
}
Some of these functions might seem redundant, but they are intended to double-check and ensure that the user is not a previous user. Is there any way to be certain that this will work when the app is downloaded from the App Store?
Thanks in advance!
StoreKit
RSS for tagSupport in-app purchases and interactions with the App Store using StoreKit.
Post
Replies
Boosts
Views
Activity
I'm adding my first in-app purchase to an app, and I'm finding the process incredibly frustrating.
Aside from the Apple Developer Documentation not being clear enough, and kind of glossing over the technical steps required (and their sample code being woefully inadequate), App Store Connect and the testing sandbox simply don't work as they say they do.
For example, in my app I've purchased the IAP and now I want to request a refund. I select the purchase, I choose a refund reason, and this page says, "To set up a test for approved refunds, select any refund reason on the refund request sheet, and submit the sheet. The App Store automatically approves the refund request in the testing environment."
Well, when I re-launch the app the purchase is still there. I can't request a refund again because it says this is a duplicate refund request, so it knows that the purchase has had a request, and it's supposed to have automatically refunded it, but it clearly hasn't.
So, I try clearing the purchase history via the Settings app > Developer > Sandbox Apple Account. Same thing. Purchase remains.
Try clearing purchase history in App Store Connect. Same thing.
How on Earth does anyone get an in-app purchase to work when the entire testing environment is so badly executed?
How do I get past this? The IAP is the last part of the app that needs to be implemented, and I've lost a week on this already.
Stripe offers variable payment structures, also known as "irregular recurring payments," which include:
Usage-based billing: Charges amounts based on usage during the billing cycle (e.g., minutes used or energy consumed).
Quantity-based billing: Charges a pre-agreed amount based on quantity (e.g., number of users in a subscription).
Is it possible to implement this type of billing in the Apple Store for apps? How would variations in amounts be handled?
We previously had a non-renewing subscription. I think we remove it. However, the subscription is still listed on the app's page in the App Store. How can I remove this mention?
The availability is still checked for all countries.
Thanks,
Cody.
I'm trying to implement my first in-app purchase, and I've created the IAP in App Store Connect, and created a sandbox account.
When I open Settings > Developer in the iPhone Simulator, there is a "Sandbox Apple Account" option at the bottom.
If I click the blue "Sign In" link I'm asked for the email and password, so I enter the correct credentials for the sandbox account. The "Sign In" text goes grey for a few seconds, then it goes blue again. It never changes to show that I'm signed in. Should it? (I think so.)
Do I need to sign into the Simulator's Apple Account, too? (I don't think so.)
Anyway, aside from that, in my app the IAP is listed and I have a button to purchase it. When I click it I'm asked for the email and password to sign into the Apple Account. I enter the correct sandbox email and password (they are definitely correct) and I see this in the Xcode console:
Purchase did not return a transaction: Error Domain=ASDErrorDomain Code=530 "(null)" UserInfo={client-environment-type=Sandbox, NSUnderlyingError=0x600000d0c7b0 {Error Domain=AMSErrorDomain Code=100 "Authentication Failed The authentication failed." UserInfo={NSMultipleUnderlyingErrorsKey=(
"Error Domain=AMSErrorDomain Code=2 \"Password reuse not available for account The account state does not support password reuse.\" UserInfo={NSDebugDescription=Password reuse not available for account The account state does not support password reuse., AMSDescription=Password reuse not available for account, AMSFailureReason=The account state does not support password reuse.}",
"Error Domain=AMSErrorDomain Code=0 \"Authentication Failed Encountered an unrecognized authentication failure.\" UserInfo={NSDebugDescription=Authentication Failed Encountered an unrecognized authentication failure., AMSDescription=Authentication Failed, AMSFailureReason=Encountered an unrecognized authentication failure.}"
), AMSDescription=Authentication Failed, NSDebugDescription=Authentication Failed The authentication failed., AMSFailureReason=The authentication failed.}}}
Why is it talking about password reuse? AFAIK, I have only one sandbox account for this app (and none for any of my other apps), and this is the only one of my apps that has an IAP.
Any ideas on how to get an IAP working? Thanks!
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.
Hello everyone,
I have some questions regarding the behavior of the offerIdentifier property in the TransactionPayload from App Store Server Notifications.
When a user redeems an Offer Code, the offerIdentifier field is populated with the respective identifier. However, I am unsure how this field behaves in different scenarios, and I would appreciate any insights or clarification:
Does the offerIdentifier persist throughout the subscription lifecycle (from the initial purchase to expiration)?
Does it become null once the Offer Code benefits expire?
Is it only present at the time of purchase and omitted in subsequent notifications?
Additionally, I would like to understand the behavior of the offerIdentifier in the following scenario:
A user purchases a lower-tier subscription using an Offer Code. Later, they upgrade to a higher-tier plan, causing the Offer Code benefits to effectively expire.
What happens to the offerIdentifier in the transaction for the upgrade?
Will it still appear in transactions after the upgrade, or will it be null?
I couldn't find explicit details about these situations in the official documentation, so I hope someone here might have experience or knowledge to share.
Thank you in advance for your help!
The app subscription function uses StoreKit. After canceling the subscription, I try to subscribe again and get the following error. I remember it was working fine before iOS 18 was released.
{
NSLocalizedDescription = "\U53d1\U751f\U672a\U77e5\U9519\U8bef";
NSUnderlyingError = "Error Domain=ASDErrorDomain Code=825 "(null)"";
}
Hope you can help me solve this problem as soon as possible. Thanks
Hi,
We use the (now deprecated) server-side receipt verification API (*1) with the app we are maintaining and there are some points I would like confirm on how its response changes based on whether the purchase being processed was a subscription that used an offer code or not.
I am particularly concerned about the following:
Are there any properties of the response that are added or missing?
Is there any property indicating that an offer code was used?
If there is, which field is that and what values does it take?
Are there any special steps or options required when processing the receipt which used an offer code on the server side?
*1 https://developer.apple.com/documentation/appstorereceipts/verifyreceipt
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)")
}
}
Hi everybody 👋 ! Just as the title says, for some reason I can no longer enter my Sandbox account credentials, because the section is gone from the developer settings. I tried reenabling the Developer mode, but with no result. Not a lot of information is available on this topic for the latest iOS versions. Can somebody assist, please?
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.
How would you go about handling this sort of situation?
An app has two tiers of non-consumable in-app purchases. The IAP simply unlocks a certain level of access in the app:
The first tier for $1.99 allows the user to add up to 50 things.
The second tier for $3.99 allows the user to add up to 200 things.
If the user has not bought an IAP the app will show the two tiers available for purchase.
The user then buys Tier 1 and happily goes about adding some things to the app.
The app now only shows Tier 2 available for purchase, because Tier 1 has been purchased.
A few weeks go by and they realise they need to add more than 50 things.
Would the user have to suck it up and just accept they should've paid the $3.99?
Or, could a new Tier 1.5 be added that's a kind of upgrade price of $2.00 (the difference between the two original tiers) to unlock the higher 200 things level? I doubt this would work properly, because although I can control that tier being displayed or not in the app, I cannot control it in the App Store product pages, and it would be displayed among the Tier 1 and Tier 2 levels, so people would just buy that rather than the full priced Tier 2.
How should I handle this situation? Just have the one tier (Tier 2) and make it simpler?
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
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!
I tried to get this post into the StoreKit forum because this issue is relative to In-App Purchases.
My App has In-App Purchases, which work, no issues here.
My App has been on the App Store for a number of years, with changes along the way. Recently, I uploaded V5.1 (Lottery Snitch) for review and the reviewer found something that had eluded everyone, until now.
Since my App has In-App Purchases, of course I have Restore In-App Purchases as a User selectable function, on the menu at the top.
The reviewer reported my App as crashing when this option was selected, which was a new thing since my App has been functioning for years.
Skipping the next several communications and moving on to the most current findings..
If my App is put onto a Mac, iMac.. Where the User has never used my app before (this eliminates leftover data files), if the User then logs out of their Apple ID prior to running my app, starts my app, selects Restore In-App Purchases the User is then presented with Apple's Request to Log-In (this has nothing to do with me..not my code..it is all 100% Apple Login request). Now, completely ignore the request for login, allow my App to complete its wait period, the User can execute any task they wish. The App runs just fine. As soon as the User selects 'Cancel' on the Apple ID login pop-up screen, my App crashes.
The Apple Login request is triggered by the restoreCompletedtransactions function for the StoreKit. The crash report indicates the DispatchQueue was the code running at the time. Thing is, my code has no DispatchQueue running. When the wait-timer completes (obvious on-screen loop) my code has zero Dispatch's running. When my code called the restoreCompletedTransactions it was not inside a Dispatch of my creation.
Anyone see this before? Anyone have a suggestion how to make this stop?
FYI, go ahead and login to your Apple ID when prompted and everything completes just fine. Yes, this problem exists in the current version(V5.0) available for download on the AppStore. It would take another post just as long to explain how this slid by on Development machines, just as weird.
What to do?
(JSYK:The App does not crash during development when running inside Xcode)
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.
Our macOS app has one in-app purchase (IAP) implemented using StoreKit 1. It works for us and beta testers but App Review get SKErrorDomain Error Code 0 / ASDErrorDomain Code 500 / AMSErrorDomain 305 on first attempt to make the in-app purchase.
However, the purchase succeeds at second attempt. We've looked through our entire IAP related code and App Store Connect setup but can’t find the reason. It's a standard implementation:
LegacyPaymentQueueObserver for SKPaymentQueue observation.
AppDelegate for initiation of payment queue observation on app launch
LegacyStoreKitPurchasableProduct for initiating a purchase and listening for the result
LegacyStoreKitProductsRequester for how we load the product before user can make in-app purchase. It happens this way:
PreviewResultsViewModelcalls loadProducts()on an instance of StoreKitPurchaseManager, which asks an instance of LegacyStoreKitProductsRequesterto requestProducts(forIdentifiers:)
Any guidance to resolve this would be appreciated.
Hi (from France)
I have a MacOS application which handles the App Store receipt by requesting at the url "https://buy.itunes.apple.com/verifyReceipt". From the response, I can know what are the inApps bought by the user and that suits for me.
I don't know if if I must change something in my code accordingly to this TN3118.
Does someone knows the response ?
Best regards.
We have in-app purchases live and working fine for standard subscriptions.
We also have promotional offers active for existing users (to give existing users a discount as a thank you).
Yet, regardless of the user type (existing vs new... we have tested with all types), we get the "Your account is not eligible for this offer" error message when clicking the discounted offer.
What is the logic for determining eligibility?
I'm trying to debug as it's not clear to me why this message would show up.
We are using React Native IAP.
In general, how does the eligibility check work? What conditions are being evaluated and compared? And what could break those conditions?
I appreciate your help!
DDD