SwiftyStoreKit - How to properly restore auto renewable subscriptions? - ios

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.

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 ?

Receipt validation before showing IAP-page

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
}

ios subsciptions don't auto renew in sandbox

I have implemented auto renewing subscription in my app. As per documentation, this should auto renew every few minutes up to six times a day. But it's not happening for my app.
I look for the subscription expiration date in the app receipt, and this works the first time, but it doesn't work afterwards. Is the app receipt not being automatically updated in the sandbox environment? It was my understanding that it should.
For anyone dealing with this problem in 2019 or later.
I also experienced subscriptions not being automatically renewed.
In my case auto renewals stopped working after buying 6 times within 8 hours.
Details:
A one month subscription in the Sandbox lasts 5 minutes and is automatically renewed 6 times (the entire purchase expires in 30 minutes). After that you have to buy again and the same process starts over.
For automatic renewals there is however a limit of buying 6 times for every 8 hour period.
This limit is per test user per 8 hours, so you can simply use a new test users to get around this.
Under
Settings -> iTunes & App Store
there is now a new option for Sandbox Account where you can log out and log in for a new test user. This took me a long time to figure out.
The following can be used to get the receipt from apple
static func getReceipt() -> String? {
guard let url = Bundle.main.appStoreReceiptURL,
let _ = try? Data(contentsOf: url) else {
print("no receipt exists")
return nil
}
do {
let receipt = try Data(contentsOf: url)
print("receipt-data: \(receipt.base64EncodedString(options:[]))")
return receipt.base64EncodedString(options: [])
}
catch {
print("catch error")
return nil
}
}
Instead of attempting to 'Build and Run' your App each time, try to reopen the App by reopening it on the device directly. I found this way, I can refresh the subscription receipts, based upon the prior sandbox receipt.
It appears that each time you 'Build and Run' via Xcode that any pending subscription renewals are reset. Potentially rebuilding your App is the reason that the subscription auto-renew is reset.

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'

how to understand whether I paid already or not for In-App Purchase

I am making an app where there are books and books have audio and video services.
Client want In-App Purchase for the app.
Below is how In-App will work.
If user want to buy the book, user will buy the book and once transaction is completed, automatically zip file will get downloaded (from my server where I am adding images, video, audio for book).
Once I have zip, I am un-zipping it once download is completed.
For me the question is how can I identify whether user xyz purchased the book before or not.
e.g. Today User A paid for Book B. How can I get info that User A have purchased book?
If I could have user registration system, I could have identified user by userid of my system.
Some say, save the book id in NSUserDefaults, but concern is if I delete the app and install again, I should download the Book B for free.
I just want to know, how can I keep the track of which user downloaded which book.
I am making app for iOS 6 and 7 both
EDIT:
Your app starts the process by calling the restoreCompletedTransactions method of SKPaymentQueue. This sends a request to the App Store to restore all of your app’s completed transactions. If your app sets a value for the applicationUsername property of its payment requests, as described in “Detecting Irregular Activity,” use the restoreCompletedTransactionsWithApplicationUsername: method to provide the same information when restoring transactions.
The App Store generates a new transaction for each transaction that was previously completed. The restored transaction has a reference to the original transaction: instances of SKPaymentTransaction have a originalTransaction property, and the entries in the receipt have an Original Transaction Identifier field.
Your transaction queue observer is called with a status of SKPaymentTransactionStateRestored for each restored transaction, as described in “Waiting for the App Store to Process Transactions.” The action you take at this point depends on the design of your app.
If your app uses the app receipt and doesn’t have Apple-hosted
content, this code isn’t needed because your app doesn’t restore
completed transactions. Finish any restored transactions immediately.
If your app uses the app receipt and has Apple-hosted content, let
the user select which products to restore before starting the
restoration process. During restoration, re-download the
user-selected content and finish any other transactions immediately
.
NSMutableArray *productIDsToRestore = <# From the user #>;
SKPaymentTransaction *transaction = <# Current transaction #>;
if ([productIDsToRestore containsObject:transaction.transactionIdentifier]) {
// Re-download the Apple-hosted content, then finish the transaction
// and remove the product identifier from the array of product IDs.
} else {
[[SKPaymentQueue defaultQueue] finishTransaction:transaction];
}
Source

Resources