Receipt validation before showing IAP-page - ios

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
}

Related

Non-consumable IAP receipt verification

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 ?

SwiftyStoreKit - How to properly restore auto renewable subscriptions?

So I have a really basic concept of auto-renewable subscriptions in the app which unlocks PRO functionality.
I also have a non-consumable purchase which is a "one-time purchase subscription" which unlocks the PRO functionality once and forever.
What I can't wrap my head around is, when I call restorePurchases (using SwiftyStoreKit) it brings in all purchases that were ever made by a user. Those may include multiple purchases of the same subscription which were expired long ago.
What I do next is I call a verifyPurchase method on each of those restored purchases and that method checks expiration date of each subscription that was ever purchased and if it is expired - it takes away the PRO from the user by clearing keychain (as it thinks that the current subscription got expired):
case .expired(let expiryDate, let items):
log.info("Subscription has expired on \(expiryDate)")
log.debug(items)
self.activeSubscription = nil
self.clearStore()
seal.reject(SubscriptionServiceError.expired(on: expiryDate))
}
So what currently happens in my app is even if a user has an active subscription, if he or she tries to restorePurchases, there is a chance that verifyPurchase last purchase to verify would be an expired subscription, resulting in a canceled PRO version of the app for the user, even with an active subscription on board.
What would be the best practice to avoid this mistake and always verify the one and only right subscription?
From the SwiftyStoreKit docs:
let purchaseResult = SwiftyStoreKit.verifySubscription(
ofType: .autoRenewable, // or .nonRenewing (see below)
productId: productId,
inReceipt: receipt)
switch purchaseResult {
case .purchased(let expiryDate, let items):
print("\(productId) is valid until \(expiryDate)\n\(items)\n")
case .expired(let expiryDate, let items):
print("\(productId) is expired since \(expiryDate)\n\(items)\n")
case .notPurchased:
print("The user has never purchased \(productId)")
}
It looks like you would pass in the product Id to determine if a specific subscription is still active.
You probably don't want to do this on every app launch so you should cache the result somewhere - all you need is probably the expiration date.

In App Purchase working fine in testing mode but not working in app review

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.

How to detect if user cancel auto-renewable subscriptions during the free trial period?

Apple's document does not seem to mention this. So if user cancels an auto-renewable subscription purchased during the free trial period, how do we detect?
In appstore receipt JSON there is this field: is_trial_period. But I think this is for indication of whether the free trial period is over.
The only thing I can think of is this NSBundle.mainBundle().appStoreReceiptURL?.path and if this is nil than that will indicate the user has not subscribed or cancel within the free trial period. But for sandbox testing, there is no way to do a cancel during free trial period to test this scenario.
Does anyone have a solid knowledge of this?
In order to support auto-renewing subscriptions, your app needs to periodically submit the app receipt obtained from NSBundle.mainBundle().appStoreReceiptURL?.path to Apple's receipt validation service.
Contained in the JSON response from this service is the latest_receipt_info array.
By examining this array you will be able to determine the currently active subscription(s).
If a user turns off auto-renewal before the expiration of the free trial then latest_receipt_info won't contain a purchase with an expires_date after the free trial end date
This means, that strictly speaking, you can't "detect a cancellation" as there is no "cancellation"; there just isn't a renewal at the end of the free trial period.
This is possible with the web hook feature in iTunes Connect.
When you set a path for the Subscription Status URL of your app the App Store server will call that URL when the subscription status changes.
Currently the following key events will trigger the call:
INITIAL_BUY Initial purchase of the subscription.
CANCEL Subscription was canceled by Apple customer support.
RENEWAL Automatic renewal was successful for an expired subscription.
INTERACTIVE_RENEWAL Customer renewed a subscription interactively after it lapsed, either by using your app’s interface or on the App
Store in account settings.
DID_CHANGE_RENEWAL_PREFERENCE Customer changed the plan that takes affect at the next subscription renewal.
DID_CHANGE_RENEWAL_STATUS Subscription has expired and customer resubscribed to the same or another plan.
More can be found here and here.
The correct way to do this is to check the auto-renew preference on the receipt. If you want to get notified of this even if the user doesn't open your app (or deletes it) you'll need to store and refresh the receipt on your server. There are 3 fields that you should be concerned with to detect a cancellation.
Expiration Date (lets you know if subscription is still active)
Auto-renew status (lets you know if the user "cancelled")
Cancellation Date (tells you why subscription cancelled by support)
You should check for receipts that are not expired, not cancelled and have an auto-renew status of "0". These will be users that are in a free trial, but have auto-renew turned off. Unfortunately, the App Store Connect Subscription Status Notifications don't report this to you.
Here's a good blog post that goes over a little more of the the details: iOS Subscriptions are Hard
Here is how I got the json data about receipt:
let sharedSecret = "..." //you can find this string on AppStoreConnect-IAP
if let appStoreReceiptURL = Bundle.main.appStoreReceiptURL,
FileManager.default.fileExists(atPath: appStoreReceiptURL.path) {
do {
let receiptData = try Data(contentsOf: appStoreReceiptURL, options: .alwaysMapped)
let receiptString = receiptData.base64EncodedString(options: [])
AF.request("https://sandbox.itunes.apple.com/verifyReceipt", method: .post, parameters: ["receipt-data":receiptString, "password": sharedSecret], encoding: JSONEncoding.default, headers: nil)
.responseJSON(completionHandler: { (response) -> Void in
print(response)
})
}
catch {
print("Couldn't read receipt data with error: " + error.localizedDescription) }
}
If you want a production URL, not the Sandbox, change the url to https://buy.itunes.apple.com/verifyReceipt
In the response you will find all data you need.
Note that you need to include this in the Podfile : pod 'Alamofire', '~> 5.2'

What should I do if IAP validation fails?

In iOS, if the user attempts to purchase IAP, and my server validation of the receipt fails, what is the proper behavior? When testing in sandbox, I just get a ton of pop ups asking for my password. If I call finishTransaction, it stops asking for my password, but I believe this can lead to the user getting charged without receiving a product.
I think that error handling when doing in-app purchases is one of the lesser talked about difficulties of doing in-app purchases. There are several questions here. For example: 1) If you are doing validation of the purchase receipt on the device (i.e., the >= iOS7 style decryption style validation), and that validation fails, what should you do? 2) If you are doing validation with a web-based server, and that fails, what should you do? 3) If you are storing receipts to your own web-based server, and that fails, what should you do? I'll put some ideas here from my own app. It would be great to see what others are doing about this kind of error handling.
1) If you are doing validation of the purchase receipt on the device (i.e., >= iOS7 decryption style validation), and that validation fails, what should you do?
Apple recommends that you do a receipt refresh (SKReceiptRefreshRequest) if your initial validation, on the device, fails. But what if the validation after the refresh fails? You could interpret that situation as there being something seriously wrong (e.g., the user has somehow hacked into your app to intentionally give you a bad receipt), tell the user the purchase has failed with a receipt validation failure, and finish the transaction (SKPaymentQueue finishTransaction:) , failing permanently. However, I don’t like to be so final. Perhaps it’s my confidence, but something could have gone wrong with my programming. I like to give the user more chances. So, my solution is to: 1) Tell the user that validation has failed, 2) put the SKPaymentTransaction into a “holding” state, and 3) call finishTransaction. This idea of a holding state is my invention, and nothing suggested by (or supported by) Apple. I have made a mutable subclass of SKPaymentTransaction, and have a queue of those objects (call them SPASMutablePaymentTransaction), which I store (using NSCoding) into NSUserDefaults. Nearly always, this queue is empty, but if I get a validation failure like I’m talking about here, I create a SPASMutablePaymentTransaction object, copy over the SKPaymentTransaction info (including the receipt, e.g., from the bundle), and save that transaction into my holding state queue. I make a (normally hidden) part of my UI visible, which can allow the user to retry holding state transactions.
Complicated? Yes, a little. However, since we’re dealing with the user giving you money, I am trying to be robust. It seems to work well so far in testing. I don’t have any feedback from users on this yet (or analytics), but it is deployed to the app store.
2) If you are doing validation with a web-based server, and that fails, what should you do?
For me, this is a similar case to 1), above. You have tried to do a validation of the receipt and it failed. You could first try to refresh the receipt (see above), and then re-try your server validation. In my app, since I always do on-device receipt validation first, so it doesn’t make sense for me to refresh the receipt again (since I would have done that if the on-device receipt validation failed). So, I again put the receipt into a “holding” state (as above), and allow the user to retry the holding state transactions at their discretion.
3) If you are storing receipts to your own web-based server, and that fails, what should you do?
(a) Presumably this should definitely not be a case where you permanently fail to give the user their purchase. This is a failure of your services. In my case this can happen, for example, if I get a network error in communicating with my server. I do something a little different here. I don’t immediately give the user access to their purchase, and I don’t put the transaction into a “holding” state. Instead, I set up a timer to retry the process of storing the receipt to my server. It retries again in a couple of minutes. I tell the user what I’m doing. I store the receipt data into NSUserDefaults (again using a SPASMutablePaymentTransaction object), so will persist in this retrying even if the app crashes/is terminated. When I do succeed in saving the receipt to my server, I give the user access to the purchase.
(b) In this case, I do call finishTransaction: and I don't yet give the user access to the purchase. I could avoid some these details by not calling finishTransaction:, and just let my transaction observer deal with this process again (e.g., when the app next starts), but the user would probably have to enter their Apple Id again. My figuring is that this is my problem (i.e., I failed to save the receipt info to my server), so I'm trying to handle it under the covers.
Pop Up A UIAlertView. See Code Below...
self.alert("Purchase Failed", Message: "Please Make Sure You are connected to the internet and try agian")
func alert(title:String, Message:String){
let actionSheet:UIAlertController = UIAlertController(title: "\(title)", message: "\(Message)", preferredStyle: UIAlertControllerStyle.Alert)
// for alert add .Alert instead of .Action Sheet
// start copy
let firstAlertAction:UIAlertAction = UIAlertAction(title: "OK", style: UIAlertActionStyle.Default, handler:
{
(alertAction:UIAlertAction!) in
// action when pressed
})
actionSheet.addAction(firstAlertAction)
// end copy
self.presentViewController(actionSheet, animated: true, completion: nil)
}

Resources