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 ?
Related
I have implemented auto-renewal subscription in iOS by swift language. The plan is for 1 year and has 3 days trial, the purchase is working perfectly. For checking if user has already buy a subscription or not, for that I am restoring purchases every time when app will open, and that also works. but problem is even subscription is expired or i have canceled from Settings > App Store > Sandbox account > Manage > Subscription then also restored the purchase successfully. I think should be not work right?
I am using SwiftyStoreKit package for handle in-app purchase.
How should I restore purchases?
SwiftyStoreKit.restorePurchases(atomically: true) { results in
if results.restoreFailedPurchases.count > 0 {
print("Restore Failed: \(results.restoreFailedPurchases)")
}
else if results.restoredPurchases.count > 0 {
print("Restore Success: \(results.restoredPurchases)")
}
else {
print("Nothing to Restore")
}
}
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.
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
}
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!
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.