I am implementing in-app purchases in a Swift 3.0 app so I need to grab the app receipt to verify it against the iTunes store. Here is how I am getting the receipt:
func getReceipt() -> Data? {
if Bundle.main.appStoreReceiptURL != nil {
print("app receipt: \(Bundle.main.appStoreReceiptURL)")
do {
let receiptData = try Data(contentsOf: Bundle.main.appStoreReceiptURL!)
print(receiptData)
return receiptData
} catch {
print("error converting receipt to Data: \(error.localizedDescription)")
}
}
return nil
}
my console output for the receipt URL is:
app receipt: Optional(file:///Users/dustinspengler/Library/Developer/XCPGDevices/433E8E8F-B781-4ADC-A92D-5CABC28E94D6/data/Containers/Data/Application/C25BE9B6-FB64-4D49-9CF2-9DA371060A7B/StoreKit/receipt)
It then failed to convert the receipt to Data and the catch statement prints:
error converting receipt to Data: The file “receipt” couldn’t be opened because there is no such file.
I get the exact same output when running this in a playground, simulator, and real devices so does this mean that no receipt exists for the app considering the fact that the user has not made an in-app purchase yet? When reading through Apple's documentation I got the impression that they are always created created regardless of prior purchases.
This answer is based off the context that the app is being run on your local machine. If the app is live in the app store, a receipt will be in place the moment you download the app even if its free.
Answered by #Paulw11:
There is no receipt until the user makes a purchase. For an app downloaded from the App Store (even a free one). This is a purchase, so there will be a receipt. For a debug build from Xcode there is no receipt until an in-app purchase is made.
Related
I have one non-consumable IAP in iOS app (premium update) currently purchase ad restore are performed without receipt verification , i come along some articles that say that an IAP can be hacked in a jailbroken devices ,and i need to avoid this issue so
1- Should I call this verify method every time the user opens the app SwiftyStoreKit
let appleValidator = AppleReceiptValidator(service: .production, sharedSecret: "xxxxxxxxxxxx")
SwiftyStoreKit.verifyReceipt(using: appleValidator, forceRefresh: true) { result in
switch result {
case .success(let receipt):
print("success: \(receipt)")
case .error(let error):
print("Verify receipt failed: \(error)")
}
}
To make sure app is truly paid and IAP not hacked ?
2- Also inside the success block i should open the app premium features , while inside failure i should lock them ?
On an app that was using the old API for In-App Purchases (StoreKit 1). The app is already published on the App Store. The purchase is non-consumable.
While trying to migrate to StoreKit 2, I'm unable to restore purchases.
Specifically displaying and purchasing products works as expected, but when deleting and reinstalling the app, and then trying to restore purchases I can't do it.
I'm trying to restore them using the new APIs but it doesn't seem to be working.
What I have tried so far:
I'm listening for transaction updates during the whole lifetime of the app, with:
Task.detached {
for await result in Transaction.updates {
if case let .verified(safe) = result {
}
}
}
I have a button that calls this method, but other than prompting to log in again with the Apple ID it doesn't seem to have any effect at all:
try? await AppStore.sync()
This doesn't return any item
for await result in Transaction.currentEntitlements {
if case let .verified(transaction) = result {
}
}
This doesn't return any item
for await result in Transaction.all {
if case let .verified(transaction) = result {
}
}
As mentioned before I'm trying this after purchasing the item and deleting the app. So I'm sure it should be able to restore the purchase.
Am trying this both with a Configuration.storekit file on the simulator, and without it on a real device, in the Sandbox Environment.
Has anyone being able to restore purchases using StoreKit 2?
PD: I already filed a feedback report on Feedback Assistant, but so far the only thing that they have replied is:
Because StoreKit Testing in Xcode is a local environment, and the data is tied to the app, when you delete the app you're also deleting all the transaction data for that app in the Xcode environment. The code snippets provided are correct usage of the API.
So yes, using a Configuration.storekit file won't work on restoring purchases, but if I can't restore them on the Sandbox Environment I'm afraid that this won't work once released, leaving my users totally unable to restore what they have already purchased.
After releasing to the App Store and finally trying the app directly in production I can confirm that it works, but I have to say that it is not ideal to be unable to test this on the sandbox environment.
Also I feel the documentation was not clear enough, at least not for me.
Probably it is clear for other folks, but I was expecting the purchases to be restored automatically and get them on for await result in Transaction.updates, but this didn't work.
What did work was to check Transaction.currentEntitlements, and if the entitlement is not there, then do a sync() and check again.
This is the code that worked for me:
try? await AppStore.sync()
for await result in Transaction.currentEntitlements {
if case let .verified(transaction) = result {
// ...
}
}
Caveats
As mentioned before, this only works on release mode and doesn't work on debug mode without StoreKit Testing, that is without a Configuration.storekit.
If you want to use StoreKit Testing (using a Configuration.storekit) restoring purchases works (with this same approach), but only if you don't delete de app and reinstall again. By deleting the app you loose StoreKit Testing history.
As mentioned by #loremipsum, this can be tested on TestFlight too (for it being release mode).
Verified both iOS and macOS.
I have a question about our in-app purchase flow because it was rejected by Apple.
Because we don’t know if the user has a valid subscription, we have to fetch the receipt first which leads to the iTunes Store password prompt (which is the normal behavior as we understand correctly in the not production environment). Then the validation comes and, depending on the result, we show the auto-renewing subscription page or we pass the requested action.
Our flow is:
Fetching receipt
Validating receipt
If
valid: pass requested action;
not valid: show auto-renewing subscription page where the user can make the purchase.
Now Apple has commented in their rejection that they don't see the auto-renewing subscription page. Instead, they got the password prompt from iTunes Store.
As we understand correctly, the password prompt is the normal behavior in a not production environment but Apple seems to not expect this behavior. We changed nothing in the way how we fetch the receipt.
We're using SwiftyStoreKit for an easy handling. This is our code:
SwiftyStoreKit.fetchReceipt(forceRefresh: false) { result in
switch result {
case .success(let receiptData):
let encryptedReceipt = receiptData.base64EncodedString(options: [])
Log.info("Fetch receipt success")
//further code to send the receipt to our server
case .error(let error):
observer.send(error: error.localizedDescription)
}
}
Is our flow incorrect or how can we validate if the user has a valid subscription without fetching the receipt? We're a bit confused here. Can someone can give us any advice here?
Your current flow doesn't give a very good user experience; they start your app and see an iTunes account password prompt without any context as to why it is appearing.
I would suggest you adopt a process similar to the following:
When you validate a subscription on this device, add a boolean to UserDefaults
When your app starts, check for this value.
If the value is true then refresh the receipt and validate the subscription
If the value is false/not present then display your store interface. Make sure that your store interface includes a "Restore" button.
Now, if the user knows that they have subscription they can tap restore and you can validate their subscription; If they are prompted for their iTunes account password at this time then they understand why.
If the user doesn't have a subscription then they can purchase one. Even if they do have one and initiate a purchase, StoreKit will handle that and tell them they already have an active subscription
Whether the subscription is purchased or restored, you will get a transaction for the subscription. You can then update UserDefaults and proceed as per step 3.
Alternatively, check to see if the local receipt exists before attempting to refresh it. If the receipt doesn't exist then it is likely that the user will be prompted for an iTunes password:
if let receiptDataURL = appStoreReceiptURL,
let _ = try? Data(contentsOf: receiptDataURL) {
SwiftyStoreKit.fetchReceipt(forceRefresh: false) { result in
switch result {
case .success(let receiptData):
let encryptedReceipt = receiptData.base64EncodedString(options: [])
Log.info("Fetch receipt success")
//further code to send the receipt to our server
case .error(let error):
observer.send(error: error.localizedDescription)
}
}
} else {
// Display purchase/restore interface
}
I integrated in app purchase in my app and i tested it with created test account. It was working fine but when i submitted it to app store for live in app review my app got rejected due to:
Guideline 2.1 - Performance - App Completeness
When validating receipts on your server, your server needs to be able to handle a production-signed app getting its receipts from Apple’s test environment. The recommended approach is for your production server to always validate receipts against the production App Store first. If validation fails with the error code “Sandbox receipt used in production,” you should validate against the test environment instead.
From WWDC 2013 Session 308 at 44:00:
"The app reviewers are actually testing your production signed, ready-to-go-into-the-store binary, but against the test environment. So your production signed app will see test environment receipts".
What this means is that if you're using a server to validate your receipt, make sure to validate the receipt against the production server first, and if that validation fails, then validate the receipt against the sandbox server.
This is what is instructed to do in the rejection reason you received:
"The recommended approach is for your production server to always validate receipts against the production App Store first. If validation fails with the error code “Sandbox receipt used in production,” you should validate against the test environment instead."
Please also see the following answers to similar questions:
https://stackoverflow.com/a/49398571/5948155
https://stackoverflow.com/a/21515982/5948155
Actually I am using this function to purchase:
func purchase(_ purchase: RegisteredPurchase) {
self.view.showLoad()
// NetworkActivityIndicatorManager.NetworkOperationStarted()
SwiftyStoreKit.purchaseProduct(bundleId + "." + purchase.rawValue, atomically: true) { result in
// NetworkActivityIndicatorManager.NetworkOperationFinished()
self.view.hideLoad()
print(result)
if case .success(let purchase) = result {
self.refrenceNumber = purchase.transaction.transactionIdentifier!
// print(purchase.transaction.transactionDate)
self.sendRefrence()
if purchase.needsFinishTransaction {
SwiftyStoreKit.finishTransaction(purchase.transaction)
}
}else{
if let alert = self.alertForPurchaseResult(result) {
self.showAlert(alert)
}
}
}
}
It working fine in testing environment but in review its response is :
paymentInvalid
How can i change environment for purchase.
Actually I understand the issue, but when I am purchasing in app purchase product. I am not validating receipt. I am direct purchasing and after calling purchase function as i updated above it gives error .paymentinvalid. where i have to validate or change or where i have to give sandbox or production url to purchase or check. Because my function is not checking any thing directly gives error.
In a previous version of my app I had in app purchases(IAPs) working fine. In my latest release I am getting some strange inconsistencies reported to me by users.
The IAP simply grants access to the "Pro" version of the app that removes ads and allows users to play audio files while the app is backgrounded.
The Issue:
Users can successfully purchases the IAP within the app. They receive the message stating that the purchase was successful. However when they restart the app (as instructed to do by the successful purchase UIAlert) the Pro features are not unlocked.
When the users then use the "Restore Purchases" button, they receive an error message stating that "Nothing to restore" basically tell them that they have not purchased anything.
The interesting things is if they press the purchase button again, then they are told that they have already purchased the IAP and can re-download it for free. Again this doesn't unlock the Pro features.
Reported Cases
I have had numerous cases of this issue being reported to me. Users on both iOS 10 and 11 have encountered this issue. However, other users on both iOS 11 and 10 have also been able to purchase the upgrade without any problem. So I am a little confused on what could of caused the issue.
Tools
I am using SwiftyStoreKit 0.10.5 to manage my IAPs. I am on Xcode 9.0 using Swift 4.
Below is the code I use to purchase and restore purchases.
I have covered my bundleID with *** for safety reasons as I don't know what can be done with it.
// Purchase Product
func purchase(purchase: RegisteredPurchases) {
NetworkActivityIndicatorManager.NetworkOperationStarted()
SwiftyStoreKit.purchaseProduct(bundleID + "." + purchase.rawValue, completion: {
result in
NetworkActivityIndicatorManager.NetworkOperationFinished()
if case .success(let product) = result {
if product.productId == "com.************.RemoveAds" {
self.defaults.set(true, forKey: "NoAds")
}
if product.needsFinishTransaction {
SwiftyStoreKit.finishTransaction(product.transaction)
}
self.showAlert(alert: self.alertForPurchaseResult(result: result))
}
})
}
// Restore Purchases
func restorePurchases() {
NetworkActivityIndicatorManager.NetworkOperationStarted()
SwiftyStoreKit.restorePurchases(atomically: true, completion: {
result in
NetworkActivityIndicatorManager.NetworkOperationFinished()
for product in result.restoredPurchases {
if product.needsFinishTransaction {
SwiftyStoreKit.finishTransaction(product.transaction)
}
if product.productId == "com.************.RemoveAds" {
self.defaults.set(true, forKey: "NoAds")
}
print(product.productId)
self.defaults.set(true, forKey: "\(product.productId)")
}
self.showAlert(alert: self.alertForRestorePurchases(result: result))
})
}
Any information on what could be the cause of this I would really appreciate it. As I am at a bit of a lose at the moment on what the cause could be.
Thanks!