App with in-app purchase crashes when downloaded from the Appstore but not when loaded locally

I just submitted my first app that contains in-app purchases, it was recently approved. The funny thing is that I downloaded it and tried to purchase it for testing purposes and it crashes when the button that offers the in-app option is tapped. What is more interesting is that it only crashes when downloaded from the Appstore, I deleted it and loaded it from my computer/XCode and it didn't crash.


Are there any chances that the URL was changed to use the sendbox for testing purposes when the app was in review?


This is the URL I used for production:

let storeURL = NSURL(string: "https://buy.itunes.apple.com/verifyReceipt")


This is the URL I used for testing which was commented out when submitted to the Appstore:

let storeURL = NSURL(string: "https:/sandbox.itunes.apple.com/verifyReceipt")


Again, is there any chance that the URL was changed for testing purposes when the app was in review and left the testing URL?


Is there a way to know what URL is currently in use, in the Appstore?


Thanks

Replies

The procedure is to verify the receipt first with the production website and if that returns a 21007 then test it against the sandbox. You need to do that during testing because during testing you get a sandbox receipt. You can't change your code when your app is approved. Of course, verifying from within the app is now frowned upon because it can be diverted.


It takes 24-48 hours for IAPs to go live after approval and you must check off 'ready for sale' to get them to go live. An app should not 'crash' if the IAPs are not available or if the receipt does not verify - it should instead tell the user that IAPs are not available or that the receipt did not pass verification.


Also - are you using a sandbox test user for your tests or are you using a production user for your tests - you can't use the same user to make the purchase in both environments.

Post not yet marked as solved Up vote reply of PBK Down vote reply of PBK

@PBK -

EDIT: I just checked in iTunesConnect in the In-App Purchases page and it has a status of "Missing Metadata" Could this be whats causing my issue? But why was the app approved if something was missing? It has a yellow dot in the Localization section under English(US). The one thing I didn't add are th screenshots because I'm not sure what type of screenshots are needed in this section.


Thank you for your reply. The way I tested it was using the sendbox URL and commenting the production URL. I do use different user account for each testing enviroment. Let me clearify something, the crash ocurrs as soon as a button offering the in-app purchase is tapped. Here is all of the code I'm usign for my in-app process. Woud you or someone else be so kind and double check it to see if there is something that could be improved? I know I know...


Based on the code below the crash happens as soon as the accessPremiumFeature from the SettingsViewController is tapped.


I'm not sure if this IF statement is the one causing the problem because as I mentioned I cannot reproduce the promblem locally, it doesn't crash when the app is loaded directly from XCode.


IF statement from SettingsViewController.swift

if NSUserDefaults.standardUserDefaults().boolForKey("com.domain.appName"){
}else{
}


AppDelegate.swif


import StoreKit
class AppDelegate: UIResponder, UIApplicationDelegate {
    var window: UIWindow?
    var canPurchase:Bool = false
    func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool {
        if SKPaymentQueue.canMakePayments(){
            self.canPurchase = true
            IAPManager.sharedInstance.setupInAppPurchases()
        }
        return true
    }
}


SettingsViewController.swift - Here is where the crash occurrs when accessPremiumFeature is tapped.

import StoreKit

class SettingsViewController: UIViewController {
    @IBAction func accessPremiumFeature() {
        if NSUserDefaults.standardUserDefaults().boolForKey("com.domain.appName"){
            let alert = UIAlertController(title: "PRO-Version", message: "You already have the PRO version.", preferredStyle: .Alert)
            alert.addAction(UIAlertAction(title: "Ok", style: UIAlertActionStyle.Default, handler: nil))
            self.presentViewController(alert, animated: true, completion: nil)
        }else{
            var productInfo:SKProduct?
            for product in IAPManager.sharedInstance.products{
                productInfo = product as? SKProduct
            }
            let alertController = UIAlertController(title: "Premium Features", message: "Unlock all premium features for \(productInfo!.price)." + "This includes... bla, bla, bla...", preferredStyle: .Alert)
            alertController.view.tintColor = UIColor.myRedColor()
            let okAction = UIAlertAction(title: "Ok", style: .Default, handler: nil)
            let buyAction = UIAlertAction(title: "Buy", style: .Default) { (action) -> Void in
                let vc = self.storyboard?.instantiateViewControllerWithIdentifier("StoreTableView") as! StoreTableViewController
                self.presentViewController(vc, animated: true, completion: nil)
            }
            alertController.addAction(okAction)
            alertController.addAction(buyAction)
            self.presentViewController(alertController, animated: true, completion: nil)
        }
    }
}


IAPManager.swift - This is the main in-app pruchase code (Brain).


import StoreKit

// protocol to notify when user restores purchase
protocol IAPManagerDelegate {
    func managerDidRestorePurchases()
}


class IAPManager: NSObject, SKProductsRequestDelegate, SKPaymentTransactionObserver, SKRequestDelegate {
    static let sharedInstance = IAPManager()
    var request:SKProductsRequest!
    var products:NSArray!

    var delegate:IAPManagerDelegate?

    //Load product identifiers for store usage
    func setupInAppPurchases(){
        self.validateProductIdentifiers(self.getProductIdentifiersFromMainBundle())
        SKPaymentQueue.defaultQueue().addTransactionObserver(self)
    }

    //Get product identifiers
    func getProductIdentifiersFromMainBundle() -> NSArray {
        var identifiers = NSArray()
        if let url = NSBundle.mainBundle().URLForResource("iap_product_ids", withExtension: "plist"){
            identifiers = NSArray(contentsOfURL: url)!
        }
        return identifiers
    }

    //Retrieve product information
    func validateProductIdentifiers(identifiers:NSArray) {
        let productIdentifiers = NSSet(array: identifiers as [AnyObject])
        let productRequest = SKProductsRequest(productIdentifiers: productIdentifiers as! Set<String>)
        self.request = productRequest
        productRequest.delegate = self
        productRequest.start()
    }

    func createPaymentRequestForProduct(product:SKProduct){
        let payment = SKMutablePayment(product: product)
        payment.quantity = 1
        SKPaymentQueue.defaultQueue().addPayment(payment)
    }

    func verifyReceipt(transaction:SKPaymentTransaction?){
        let receiptURL = NSBundle.mainBundle().appStoreReceiptURL!
        if let receipt = NSData(contentsOfURL: receiptURL){
            //Receipt exists
            let requestContents = ["receipt-data" : receipt.base64EncodedStringWithOptions(NSDataBase64EncodingOptions(rawValue: 0))]

            //Perform request
            do {
                let requestData = try NSJSONSerialization.dataWithJSONObject(requestContents, options: NSJSONWritingOptions(rawValue: 0))

                //Build URL Request
                let storeURL = NSURL(string: "https://buy.itunes.apple.com/verifyReceipt")// production URL
                //let storeURL = NSURL(string: "https:/sandbox.itunes.apple.com/verifyReceipt") // Testing URL
                let request = NSMutableURLRequest(URL: storeURL!)
                request.HTTPMethod = "Post"
                request.HTTPBody = requestData

                let session = NSURLSession.sharedSession()
                let task = session.dataTaskWithRequest(request, completionHandler: { (responseData:NSData?, response:NSURLResponse?, error:NSError?) -> Void in
                    do {
                        let json = try NSJSONSerialization.JSONObjectWithData(responseData!, options: .MutableLeaves) as! NSDictionary
   
                        print(json)
   
                        if (json.objectForKey("status") as! NSNumber) == 0 {
                            if let latest_receipt = json["latest_receipt_info"]{
                                self.validatePurchaseArray(latest_receipt as! NSArray)
                            } else {
                                let receipt_dict = json["receipt"] as! NSDictionary
                                if let purchases = receipt_dict["in_app"] as? NSArray{
                                    self.validatePurchaseArray(purchases)
                                }
                            }
       
                            if transaction != nil {
                                SKPaymentQueue.defaultQueue().finishTransaction(transaction!)
                            }
       
                            dispatch_sync(dispatch_get_main_queue(), { () -> Void in
                                self.delegate?.managerDidRestorePurchases()
                            })
       
                        } else {
                            //Debug the receipt
                            print(json.objectForKey("status") as! NSNumber)
                        }
                    } catch {
                        print(error)
                    }
                })
                task.resume()
            } catch {
                print(error)
            }
        } else {
            //Receipt does not exist
            print("No Receipt")
        }
    }

    func validatePurchaseArray(purchases:NSArray){
        for purchase in purchases as! [NSDictionary]{
            self.unlockPurchasedFunctionalityForProductIdentifier(purchase["product_id"] as! String)
        }
    }


    func unlockPurchasedFunctionalityForProductIdentifier(productIdentifier:String){
        NSUserDefaults.standardUserDefaults().setBool(true, forKey: productIdentifier)
        NSUserDefaults.standardUserDefaults().synchronize()
        UIApplication.sharedApplication().networkActivityIndicatorVisible = false
    }

    func lockPurchasedFunctionalityForProductIdentifier(productIdentifier:String){
        NSUserDefaults.standardUserDefaults().setBool(false, forKey: productIdentifier)
        NSUserDefaults.standardUserDefaults().synchronize()
        UIApplication.sharedApplication().networkActivityIndicatorVisible = false
    }

    //MARK: SKProductsRequestDelegate
    func productsRequest(request: SKProductsRequest, didReceiveResponse response: SKProductsResponse) {
        self.products = response.products
        print(self.products)
    }

    // MARK: SKPaymentTransactionObserver Protocol
    func paymentQueue(queue: SKPaymentQueue, updatedTransactions transactions: [SKPaymentTransaction]) {
        for transaction in transactions as [SKPaymentTransaction]{
            switch transaction.transactionState{
            case .Purchasing:
                print("Purchasing")
                UIApplication.sharedApplication().networkActivityIndicatorVisible = true
            case .Deferred:
                print("Deferrred")
                UIApplication.sharedApplication().networkActivityIndicatorVisible = false
            case .Failed:
                print("Failed")
                print(transaction.error?.localizedDescription)
                UIApplication.sharedApplication().networkActivityIndicatorVisible = false
                SKPaymentQueue.defaultQueue().finishTransaction(transaction)
            case.Purchased:
                print("Purchased")
                self.verifyReceipt(transaction)
            case .Restored:
                print("Restored")
            }
        }
    }

    func restorePurchases(){
        let request = SKReceiptRefreshRequest()
        request.delegate = self
        request.start()
    }

    func requestDidFinish(request: SKRequest) {
        self.verifyReceipt(nil)
    }
}

>But why was the app approved if something was missing? It has a yellow dot in the Localization section under English(US). The one thing I didn't add are th screenshots because I'm not sure what type of screenshots are needed in this section.

The app was approved, the IAPs were not approved


>The way I tested it was using the sendbox URL and commenting the production URL.

This makes no sense. When you submitted it for app approval it was either commented out or it was not - and both choices are wrong. You test first at production and if you get back a 21007 you test in sandbox.

>Based on the code below the crash happens as soon as the accessPremiumFeature from the SettingsViewController is tapped.

I don't do swift. Your products will be returned as nil if your IAPs are not approved. The first time your singleton returns it will always be nil since setting the value of products is done asynchronously.

Post not yet marked as solved Up vote reply of PBK Down vote reply of PBK

> The app was approved, the IAPs were not approved

I just find it weird that they don't let the develper know. By the way, do you think this could be causing the error?


> This makes no sense. When you submitted it for app approval it was either commented out or it was not - and both choices are wrong. You test first at production and if you get back a 21007 you test in sandbox.

I commented-out the production URL when I submitted the app. If I understand this correctly, the first time you test from XCode should be using the production URL and once you know it's woking (21007) you should change the URL to use the sandbox, correct? If yes, how do you check if 21007 was returened?


> I don't do swift. Your products will be returned as nil if your IAPs are not approved. The first time your singleton returns it will always be nil since setting the value of products is done asynchronously.


Do you see an issue here? Would you relate this to my issue?


Thanks a lot

@PBK

> This makes no sense. When you submitted it for app approval it was either commented out or it was not - and both choices are wrong. You test first at production and if you get back a 21007 you test in sandbox.

Could you please elaborate more on this statement? Are you saying that moth URLs should be uncommented when testing and when submiting to the appstore? Should they be inside an IF statement checking for the production first and then the sandbox?


Thanks

You first send the receipt to the production website a synchronously. In the return method you check the receipt. If it is valid you credit the IAP to the user. If it is invalid and the error is not 21007 you tell the user that their receipt is invalid. If the error code is 21007 you send the receipt to the sandbox asynchronously. In the return method you do what is stated above.