hamobi hamobi - 4 months ago 19
iOS Question

My app only crashes on App Store! Seems related to IAP receipt validation

I released an update to my game which I thought would fix receipt validation for IAP.

This all works great on my dev environment, but if you download the app from the app store, it crashes on launch. I looked at the crash report on the device but I can't tell exactly which mistake I'm making since this all works perfect if I build from my computer. Very frustrating Apple would accept my update when it crashes my app on launch!

Exception Type: EXC_CRASH (SIGABRT)
Exception Codes: 0x0000000000000000, 0x0000000000000000
Triggered by Thread: 3

Last Exception Backtrace:
0 CoreFoundation 0x1845a259c __exceptionPreprocess + 132
1 libobjc.A.dylib 0x194cf40e4 objc_exception_throw + 60
2 CoreFoundation 0x184487904 -[__NSArrayI objectAtIndex:] + 224
3 Kill Sector 0x1000b3714 0x10001c000 + 620308
4 CFNetwork 0x183f8af34 __67+[NSURLConnection sendAsynchronousRequest:queue:completionHandler:]_block_invoke_2 + 188
5 Foundation 0x18545b4a8 __NSBLOCKOPERATION_IS_CALLING_OUT_TO_A_BLOCK__ + 16
6 Foundation 0x1853acc34 -[NSBlockOperation main] + 96
7 Foundation 0x18539c5bc -[__NSOperationInternal _start:] + 636
8 Foundation 0x18545e20c __NSOQSchedule_f + 228
9 libdispatch.dylib 0x19533936c _dispatch_client_callout + 16
10 libdispatch.dylib 0x1953434c0 _dispatch_queue_drain + 1216
11 libdispatch.dylib 0x19533c474 _dispatch_queue_invoke + 132
12 libdispatch.dylib 0x195345224 _dispatch_root_queue_drain + 664
13 libdispatch.dylib 0x19534675c _dispatch_worker_thread3 + 108
14 libsystem_pthread.dylib 0x1955152e4 _pthread_wqthread + 816
15 libsystem_pthread.dylib 0x195514fa8 start_wqthread + 4


here is my check receipt code:

func checkReceipt(){

var appStoreURL = "https://buy.itunes.apple.com/verifyReceipt"

#if DEBUG
appStoreURL = "https://sandbox.itunes.apple.com/verifyReceipt"
#endif


var appStoreStatusZero = false // we connected to correct app store

if let url = NSBundle.mainBundle().appStoreReceiptURL {
if let receipt = NSData(contentsOfURL: url) {
var error: NSError?

let requestContents = ["receipt-data": receipt.base64EncodedStringWithOptions(NSDataBase64EncodingOptions(rawValue: 0))]
let requestData = NSJSONSerialization.dataWithJSONObject(requestContents, options: NSJSONWritingOptions(rawValue: 0), error: &error)
if requestData == nil {
println("not receiving request Data")

}

let storeURL = NSURL(string: appStoreURL)
let storeRequest = NSMutableURLRequest(URL: storeURL!)
storeRequest.HTTPMethod = "POST"
storeRequest.HTTPBody = requestData

let queue = NSOperationQueue()

NSURLConnection.sendAsynchronousRequest(storeRequest, queue: queue, completionHandler: {
response, data, error in
if error != nil {
println("connection error")
return
} else {
var error: NSError?
let jsonResponse = NSJSONSerialization.JSONObjectWithData(data, options: NSJSONReadingOptions(rawValue: 0), error: &error) as NSDictionary?
if jsonResponse == nil {
println("json error")
return
} else {


var receiptInfo = jsonResponse?.objectForKey("receipt") as NSDictionary?
var inAppInfo = receiptInfo?.objectForKey("in_app") as NSArray?
var thisIAPInfo = inAppInfo?.objectAtIndex(0) as NSDictionary?
var removeAdsID = thisIAPInfo?.objectForKey("product_id") as String?

if removeAdsID == self.productID {
self.adsRemoved = true
}
}
}
})


}
}

}



  1. Is there anything obviously wrong I'm doing here? I can't tell where I'm going wrong.

  2. Is there any way to test this as if it were on the app store? I've tried ad hoc deployment, but my app works fine like that. It's very hard to tell if I'm fixing the bug without being able to test!


Answer

First of all you should never validate receipt with apple servers directly from device. It is not very secure. You should choose either local receipt validation, or your server receipt validation which in case will talk to apple validation server.

I think your code is crashing because you are not checking the result of theese statements. Somewhere you are getting nil and crash.

var receiptInfo = jsonResponse?.objectForKey("receipt") as NSDictionary?
var inAppInfo = receiptInfo?.objectForKey("in_app") as NSArray?
var thisIAPInfo = inAppInfo?.objectAtIndex(0) as NSDictionary?
var removeAdsID = thisIAPInfo?.objectForKey("product_id") as String?

Try to use if let constructions. And check array bounds before getting elements, it could be empty.

Something like that:

if let receiptInfo = jsonResponse.objectForKey("receipt") as NSDictionary? {
    if let inAppInfo = receiptInfo.objectForKey("in_app") as NSArray? {
        if (inAppInfo.count > 0) {
            if let thisIAPInfo = inAppInfo.objectAtIndex(0) as? NSDictionary {
                if let removeAdsID = thisIAPInfo.objectForKey("product_id") as String? {
                    // Do what you want here
                }
            }
        }
    }
}

And to have an idea how to make it more beautiful you can read this: http://blog.scottlogic.com/2014/12/08/swift-optional-pyramids-of-doom.html