Receipt-Validation code for iOS app, not working - ios

I have implemented Receipt-Validation code for the first time in an iOS app, following the Raywenderlich tutorial
Even though it was working fine during the test, using Sandbox. In reality, it does not work as expected.
When the app is downloaded, all features are available from start; meaning there is no check on the purchase of the iap contents.
In the case of Sandbox I needed to make a purchase to get the receipt and "making a purchase" meant "getting the content of the iap".
And as far as I know, in the real world the receipt comes with the app.
My interpretation is that I am probably not checking what I should in the code.
Any guidance by an expert in the field would be very much appreciated.
Looking at the code, it seems that there is (indeed) not much check, other than making sure that their is a valid (well formed) receipt. But I guess I need more to check that the very contents of the IAP had been paid for.
Below is what I think is the relevant code:
func validateReceipt() {
receipt = Receipt()
if let receiptStatus = receipt?.receiptStatus {
guard receiptStatus == .validationSuccess else {
return
}
// If verification succeed, we show information contained in the receipt.
// print("Bundle Identifier: \(receipt!.bundleIdString!)")
// print("Bundle Version: \(receipt!.bundleVersionString!)")
if let originalVersion = receipt?.originalAppVersion {
//print("originalVersion: \(originalVersion)")
} else {
//print("originalVersion: Not Provided")
}
if let receiptExpirationDate = receipt?.expirationDate {
//print("Expiration Date: \(formatDateForUI(receiptExpirationDate))")
} else {
//print("Expiration Date: Not Provided")
}
if let receiptCreation = receipt?.receiptCreationDate {
//print("receiptCreation: \(formatDateForUI(receiptCreation))")
} else {
//print("receiptCreation: Not Provided")
}
// At this point we should enable full features if it is not yet the case.
.... code to unlock full features .....
}
}

You should check fields with type 17xx where you can find product identifier (field type 1702) and purchase date (field type 1704). This is described at Raywenderlich tutorial you mentioned in part Reading In-App Purchases.
So, you can check if user purchased IAP with product identifier you interested in. Product identifier is the same as you choose at App Store Connect at MyApps -> Feature -> IAP.

Related

Revenue Cat iOS - How do I validate purchases so that jailbroken iPhones can't crack the In App Purchase?

I just released an app for iOS and I used Revenue Cat to help with IAPs. I just found out that anyone with a jailbroken iPhone can make fake purchases that give them the "goods" without making a payment. Does Revenue Cat have a way to verify and make sure this doesn't happen?
Here is the code for when a certain is made in the app (button press):
guard let package = offering?.availablePackages[2] else {
print("No available package")
return
}
Purchases.shared.purchasePackage(package) { (trans, info, error, cancelled) in
// handle purchase
if trans?.transactionState == .purchased {
if let currentUser = Auth.auth().currentUser {
var ref: DatabaseReference!
ref = Database.database().reference()
ref.child("users/\(currentUser.uid)/score").getData(completion: { error, snapshot in
guard error == nil else {
print(error!.localizedDescription)
return;
}
let score = snapshot.value as? Int ?? 0;
let newScore = score + 100
ref = Database.database().reference()
ref.child("users").child(currentUser.uid).updateChildValues(["score": newScore])
});
}
}
}
Incrementing the score from the client is not very secure. You could listen for RevenueCat webhooks and update the score server side on a successful purchase or renewal. The webhook will only be dispatched from RevenueCat on a valid purchase that's securely verified with Apple.
Another approach to this would be to ping your server in the purchase completion block to check RevenueCat for the latest purchase status. So you ping your server, then from your server you call RevenueCat's GET /subscriber endpoint and make sure the score has been updated. Webhooks then could be used as a redundancy mechanism.

High number of users fail to purchase my IAP. Is this to be expected?

After adding analytics to my app, I have seen a very high number of users reach an error when trying to purchase a product within my app. Not all fail though.
.03% of all my users reach the error
.002% of my users successfully purchase the product
It's been very hard for me to debug because when I test with different devices and different apple accounts, the purchase always succeeds.
The error event is called when either 0 SKProducts can be found (they must have no internet?), or when they attempt to purchase, the transaction reads SKPaymentTransactionStateFailed.
My question is, how should I go about debugging this? What is the normal percentage of users that fail to purchase a product (maybe their iCloud isn't setup correctly, or their payment is declined). I still get a relatively normal amount of revenue from the IAP, so clearly it is working for some people. Am I really missing out on all these purchases due to a bug, or is something else going on?
My purchase code looks like this. I am using a pod called IAPHelper. I am highly doubtful the pod is the problem, since I've switched it out and had the same results.
- (void)makePurchase {
SKProduct* product =[[IAPShare sharedHelper].iap.products objectAtIndex:0];
[[IAPShare sharedHelper].iap buyProduct:product
onCompletion:^(SKPaymentTransaction* trans){
if(trans.error){
[self showErrorPurchasing:trans.error];
} else if(trans.transactionState == SKPaymentTransactionStatePurchased) {
[[IAPShare sharedHelper].iap provideContentWithTransaction:trans];
[self purchaseSucceeded];
} else if(trans.transactionState == SKPaymentTransactionStateFailed) {
[self showErrorPurchasing:trans.error];
} else if(trans.transactionState == SKPaymentTransactionStateDeferred) {
[self hideHud];
} else if(trans.transactionState == SKPaymentTransactionStateRestored) {
[self purchaseSucceeded];
}
}];
}
Thank you
I can't answer your question directly but in terms of the code maybe this will help. When you do
SKProduct* product =[[IAPShare sharedHelper].iap.products objectAtIndex:0];
the objectAtIndex will crash if the array is empty. Either test the array to make sure it has products or use firstObject and then test if firstObject is nil. So in summary
SKProduct * product = [... firstObject];
if ( product )
{
... your code ...
}
else
{
... unable to read products / no connection ...
}
Your users might have cancelled the purchase without continuing. In that case, you will receive purchase failed state.
Check whether the error code 2, to identify a user purchase cancel event. Here's how I handle it in my swift project. Hope this helps.
case .failed:
let errorCode = (trans.error as? SKError)?.code
if (errorCode == .paymentCancelled) {
//Handle cancelled purchase
}

Force user to update the app programmatically in iOS

In my iOS app I have enabled force app update feature. It is like this.
If there is a critical bug fix. In the server we are setting the new release version. And in splash screen I am checking the current app version and if its lower than the service version, shows a message to update the app.
I have put 2 buttons "Update now", "Update later"
I have 2 questions
If I click now. App should open my app in the appstore with the button UPDATE. Currently I use the link "http://appstore.com/mycompanynamepvtltd"
This opens list of my company apps but it has the button OPEN, not the UPDATE even there is a new update for my app. whats the url to go for update page?
If he click the button "Update Later" is it ok to close the app programmatically? Does this cause to reject my app in the appstore?
Please help me for these 2 questions
Point 2 : You should only allow force update as an option if you don't want user to update later. Closing the app programmatically is not the right option.
Point 1 : You can use a good library available for this purpose.
Usage in Swift:
Library
func applicationDidBecomeActive(application: UIApplication) {
/* Perform daily (.daily) or weekly (.weekly) checks for new version of your app.
Useful if user returns to your app from the background after extended period of time.
Place in applicationDidBecomeActive(_:)*/
Siren.shared.checkVersion(checkType: .daily)
}
Usage in Objective-C: Library
-(void)applicationDidBecomeActive:(UIApplication *)application {
// Perform daily check for new version of your app
[[Harpy sharedInstance] checkVersionDaily];
}
How it works : It used lookup api which returns app details like link including version and compares it.
For an example, look up Yelp Software application by iTunes ID by calling https://itunes.apple.com/lookup?id=284910350
For more info, please visit link
Don't close the app programmatically. Apple can reject the app. Better approach will be do not allow user to use the app. Keep the update button. Either user will go to app store or close the app by himself.
According to Apple, your app should not terminate on its own. Since the user did not hit the Home button, any return to the Home screen gives the user the impression that your app crashed. This is confusing, non-standard behavior and should be avoided.
Please check this forum:
https://forums.developer.apple.com/thread/52767.
It is happening with lot of people. In my project I redirected the user to our website page of downloading app from app store. In that way if the user is not getting update button in app store, at least the user can use the website in safari for the time being.
To specifically answer your question:
Use this URL to directly open to your app in the app store:
https://apps.apple.com/app/id########## where ########## is your app's 10 digit numeric ID. You can find that ID in App Store Connect under the App Information section. It's called "Apple ID".
I actually have terminate functionality built into my app if it becomes so out of date that it can no longer act on the data it receives from the server (my app is an information app that requires connectivity to my web service). My app has not been rejected for having this functionality after a dozen updates over a couple years, although that function has never been invoked. I will be switching to a static message instead of terminating the app, just to be safe to avoid future updates from being rejected.
I have found that the review process is at least somewhat subjective, and different reviewers may focus on different things and reject over something that has previously been overlooked many times.
func appUpdateAvailable() -> (Bool,String?) {
guard let info = Bundle.main.infoDictionary,
let identifier = info["CFBundleIdentifier"] as? String else {
return (false,nil)
}
// let storeInfoURL: String = "http://itunes.apple.com/lookupbundleId=\(identifier)&country=IN"
let storeInfoURL:String = "https://itunes.apple.com/IN/lookup?
bundleId=\(identifier)"
var upgradeAvailable = false
var versionAvailable = ""
// Get the main bundle of the app so that we can determine the app's
version number
let bundle = Bundle.main
if let infoDictionary = bundle.infoDictionary {
// The URL for this app on the iTunes store uses the Apple ID
for the This never changes, so it is a constant
let urlOnAppStore = NSURL(string: storeInfoURL)
if let dataInJSON = NSData(contentsOf: urlOnAppStore! as URL) {
// Try to deserialize the JSON that we got
if let dict: NSDictionary = try?
JSONSerialization.jsonObject(with: dataInJSON as Data, options:
JSONSerialization.ReadingOptions.allowFragments) as! [String:
AnyObject] as NSDictionary? {
if let results:NSArray = dict["results"] as? NSArray {
if let version = (results[0] as! [String:Any]).
["version"] as? String {
// Get the version number of the current version
installed on device
if let currentVersion =
infoDictionary["CFBundleShortVersionString"] as? String {
// Check if they are the same. If not, an
upgrade is available.
print("\(version)")
if version != currentVersion {
upgradeAvailable = true
versionAvailable = version
}
}
}
}
}
}
}
return (upgradeAvailable,versionAvailable)
}
func checkAppVersion(controller: UIViewController){
let appVersion = ForceUpdateAppVersion.shared.appUpdateAvailable()
if appVersion.0 {
alertController(controller: controller, title: "New Update", message: "New version \(appVersion.1 ?? "") is available")
}
}
func alertController(controller:UIViewController,title: String,message: String){
let alertController = UIAlertController(title: title, message: message, preferredStyle: .alert)
alertController.addAction(UIAlertAction(title: "Update", style: .default, handler: { alert in
guard let url = URL(string: "itms-apps://itunes.apple.com/app/ewap/id1536714073") else { return }
if #available(iOS 10.0, *) {
UIApplication.shared.open(url, options: [:], completionHandler: nil)
} else {
UIApplication.shared.openURL(url)
}
}))
DispatchQueue.main.async {
controller.present(alertController, animated: true)
}
}
Use appgrades.io. Keep your app focus on delivering the business value and let 3rd party solution do their tricks. With appgrades, you can, once SDK integrated, create a custom view/alert to display for your old versions users asking them to update their apps. You can customize everything in the restriction view/alert to make it appear as part of your app.

In app purchase testing in iOS

I am using Xcode 8.0, Swift 3.0 and testing in app purchases in my iPad. I want to test in app purchases using sandbox user.
There is no account added in device's Setting
The Problem is I am not getting product list in response of product request code.
Please take a look on my code:
let PRODUCT_ID_MY_PRODUCT = "com.company.ProjectName.MyProduct"
// The ProducID in this code and ProducID on iTunes are the SAME. ✔️
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
if productID == nil {
productID = PRODUCT_ID_MY_PRODUCT
}
SKPaymentQueue.default().add(self)
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
startPurchases()
}
func startPurchases() {
if (SKPaymentQueue.canMakePayments())
{
let productIDs = NSSet(object: self.productID!)
let productsRequest:SKProductsRequest = SKProductsRequest(productIdentifiers: productIDs as! Set<String>)
productsRequest.delegate = self
productsRequest.start()
}
}
// Delegate Methods for SKProductsRequest
func productsRequest (_ request: SKProductsRequest, didReceive response: SKProductsResponse) {
let count : Int = response.products.count
// THE PROBLEM IS HERE.. I AM GETTING COUNT IS ZERO.. MEANS response.products returning null ARRAY
if (count>0) {
let validProducts = response.products
for aProduct in validProducts {
print(aProduct.productIdentifier)
}
} else {
DispatchQueue.main.async(execute: {
UIAlertView(title: "Purchase !", message: "Product not available", delegate: nil, cancelButtonTitle: "OK").show()
return
})
}
}
So..... That's the problem: I am getting response.products null (no data in array) so Please help me to find the solution. You can see the comments in code:
// THE PROBLEM IS HERE.. I AM GETTING COUNT IS ZERO.. MEANS response.products returning null ARRAY
I created products over iTunes Connect. You can see the image below. All the products are in "Ready to Submit" state.
There is some warning on iTunes
Your first In-App Purchase must be submitted with a new app version.
Select it from the app’s In-App Purchases section and click Submit.
Once your binary has been uploaded and your first In-App Purchase
has been submitted for review, additional In-App Purchases can be
submitted using the table below.
And
I also created Sendbox user for testing In-App Purchases. See the image below:
Did I miss something? Or what is the error? And where is error? I want to test in app purchases using sandbox user
I fixed this. There are some points need to careful about. See below:
Make sure your developer account did the paid application contract. see image below:
Create Products on the iTunes Connect.
Implement In-App-Purchases code and configuration settings.
Create one build with Distribution profile.
Upload build on store. Add build to current version. Add In-App-Purchases to the version on iTunes Connect.
Then try to test, if still not then submit app once then cancel it. then after try to test on your device.
Make sure when you test with sandbox user, you need to sign-out from your already logged in account from device settings, and login with sandbox ID.
some screenshots might be helpful.
please check these settings
capabilities --> In-App purchase --> set to "ON"
and at developer.apple.com--> enable In-App purchase for App ID.
and please test app on Device instead of simulator.

iOS inapp purchase plugin return null

I'm using cordova to make an app in iOS store an I also use https://github.com/j3k0/cordova-plugin-purchase/blob/master/doc/api.md for integrate in-app purchase.
I already create product on my iTunes connect and put the code like
function initStore(){
if (!window.store) {
log('Store not available');
return;
}
// Enable maximum logging level
store.verbosity = store.DEBUG;
// Enable remote receipt validation
//store.validator = "https://api.fovea.cc:1982/check-purchase";
// Inform the store of your products
alert('registerProducts');
store.register({
id: 'com.thegiffary.product.quran_full',
alias: 'full version',
type: store.NON_CONSUMABLE
});
// When any product gets updated, refresh the HTML.
store.when("product").updated(function (p) {
alert(JSON.stringify(p))
});
// When purchase of the full version is approved,
// show some logs and finish the transaction.
store.when("full version").approved(function (order) {
alert('You just unlocked the FULL VERSION!');
order.finish();
});
// The play button can only be accessed when the user
// owns the full version.
store.when("full version").updated(function (product) {
alert(JSON.stringify(product))
});
// When the store is ready (i.e. all products are loaded and in their "final"
// state), we hide the "loading" indicator.
//
// Note that the "ready" function will be called immediately if the store
// is already ready.
store.ready(function() {
alert('ready')
});
// When store is ready, activate the "refresh" button;
store.ready(function() {
alert('refresh-button')
});
// Alternatively, it's technically feasible to have a button that
// is always visible, but shows an alert if the full version isn't
// owned.
// ... but your app may be rejected by Apple if you do it this way.
//
// Here is the pseudo code for illustration purpose.
// myButton.onclick = function() {
// store.ready(function() {
// if (store.get("full version").owned) {
// // access the awesome feature
// }
// else {
// // display an alert
// }
// });
// };
// Refresh the store.
//
// This will contact the server to check all registered products
// validity and ownership status.
//
// It's fine to do this only at application startup, as it could be
// pretty expensive.
alert('refresh');
store.refresh();
}
When I test the code on real device, it return product with null information (please see the picture)
I'm not sure am I miss any thing when I config the in-app product? Please give me any suggestion.
Regards.
I was facing a similar problem developing an android in-app-purchasing app. I am going to take a stab in the dark and guess that the problem might be similar. My issue was that the id for my product did not match what I was trying to pull from the store.
The product id and type have to match products defined in your Apple and Google developer consoles.
In my case, when the product loaded, it had the same field values as the one you are getting now. I changed the id, and it loaded perfectly. You may want to check those two values in your store.register function.

Resources