1

I have an app with just one IAP which allows the user to unlock the full game. For a minority of users, they buy the IAP successfully but when they tap to continue, the game crashes. When re-launching the app and attempting to load their saved game, the app again crashes. Deleting and reinstalling the app also makes no difference.

Here is a crash report:

Incident Identifier: B7B61633-1BE4-4AB2-99ED-A207B2E88525
CrashReporter Key:   2b01761b32c1d23c1adf755f83cc58464c9e7e77
Hardware Model:      iPhone5,2
Process:             MyApp [551]
Path:                /private/var/mobile/Containers/Bundle/Application/43D176E1-395E-4BF5-A0D5-3602068AADA6/MyApp.app/MyApp
Identifier:          com.BlahBlah.MyApp
Version:             5 (1.1)
Code Type:           ARM (Native)
Parent Process:      launchd [1]

Date/Time:           2016-03-02 02:10:42.42 +0000
Launch Time:         2016-03-02 02:10:27.27 +0000
OS Version:          iOS 9.2.1 (13D15)
Report Version:      105

Exception Type:  EXC_BREAKPOINT (SIGTRAP)
Exception Codes: 0x0000000000000001, 0x00000000e7ffdefe
Triggered by Thread:  0

Breadcrumb Trail: (reverse chronological seconds)
14     GC Framework: startAuthenticationForExistingPrimaryPlayer


Global Trace Buffer (reverse chronological seconds):
13.238455    CFNetwork                  0x0000000023da3e45 TCP Conn 0x16ede9d0 SSL Handshake DONE
13.532519    CFNetwork                  0x0000000023da3d7f TCP Conn 0x16ede9d0 starting SSL negotiation
13.534444    CFNetwork                  0x0000000023e231a5 TCP Conn 0x16ede9d0 complete. fd: 11, err: 0
13.537454    CFNetwork                  0x0000000023e242a7 TCP Conn 0x16ede9d0 event 1. err: 0
13.643210    CFNetwork                  0x0000000023e24325 TCP Conn 0x16ede9d0 started
13.648224    CFNetwork                  0x0000000023da3e45 TCP Conn 0x16ed89f0 SSL Handshake DONE
13.982238    CFNetwork                  0x0000000023da3d7f TCP Conn 0x16ed89f0 starting SSL negotiation
13.982896    CFNetwork                  0x0000000023e231a5 TCP Conn 0x16ed89f0 complete. fd: 6, err: 0
13.984447    CFNetwork                  0x0000000023e242a7 TCP Conn 0x16ed89f0 event 1. err: 0

Thread 0 name:
Thread 0 Crashed:
0   MyApp                       0x002028ac _TFFC11MyApp9IAPHelper12paymentQueueFS0_FTCSo14SKPaymentQueue19updatedTransactionsGSaCSo20SKPaymentTransaction__T_U_FT_T_ + 7504 (IAPHelper.swift:0)
1   libdispatch.dylib               0x23447dd6 _dispatch_call_block_and_release + 10 (init.c:760)
2   libdispatch.dylib               0x234514e6 _dispatch_after_timer_callback + 66 (queue.c:3293)
3   libdispatch.dylib               0x23447dc2 _dispatch_client_callout + 22 (init.c:819)
4   libdispatch.dylib               0x2345a6d2 _dispatch_source_latch_and_call + 2042 (inline_internal.h:1063)
5   libdispatch.dylib               0x23449d16 _dispatch_source_invoke + 738 (source.c:755)
6   libdispatch.dylib               0x2344c1fe _dispatch_main_queue_callback_4CF + 394 (inline_internal.h:1043)
7   CoreFoundation                  0x2386cfc4 __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__ + 8 (CFRunLoop.c:1613)
8   CoreFoundation                  0x2386b4be __CFRunLoopRun + 1590 (CFRunLoop.c:2718)
9   CoreFoundation                  0x237bdbb8 CFRunLoopRunSpecific + 516 (CFRunLoop.c:2814)
10  CoreFoundation                  0x237bd9ac CFRunLoopRunInMode + 108 (CFRunLoop.c:2844)
11  GraphicsServices                0x24a37af8 GSEventRunModal + 160 (GSEvent.c:2245)
12  UIKit                           0x27aa9fb4 UIApplicationMain + 144 (UIApplication.m:3681)
13  MyApp                           0x001898f4 main + 180 (AppDelegate.swift:12)
14  libdyld.dylib                   0x23470872 start + 2 (start_glue.s:64)

Thread 1 name:
Thread 1:
0   libsystem_kernel.dylib          0x23543320 kevent_qos + 24
1   libdispatch.dylib               0x2345794e _dispatch_mgr_invoke + 254 (source.c:2542)
2   libdispatch.dylib               0x23449a2e _dispatch_mgr_thread + 38 (source.c:2573)

Thread 2:
0   libsystem_kernel.dylib          0x2354288c __workq_kernreturn + 8
1   libsystem_pthread.dylib         0x235e0e18 _pthread_wqthread + 1036 (pthread.c:1999)
2   libsystem_pthread.dylib         0x235e09fc start_wqthread + 8 (pthread_asm.s:147)

Thread 3:
0   libsystem_kernel.dylib          0x2354288c __workq_kernreturn + 8
1   libsystem_pthread.dylib         0x235e0e18 _pthread_wqthread + 1036 (pthread.c:1999)
2   libsystem_pthread.dylib         0x235e09fc start_wqthread + 8 (pthread_asm.s:147)

Thread 4:
0   libsystem_kernel.dylib          0x2354288c __workq_kernreturn + 8
1   libsystem_pthread.dylib         0x235e0e18 _pthread_wqthread + 1036 (pthread.c:1999)
2   libsystem_pthread.dylib         0x235e09fc start_wqthread + 8 (pthread_asm.s:147)

Thread 5 name:
Thread 5:
0   libsystem_kernel.dylib          0x2352dbf8 mach_msg_trap + 20 (syscall_sw.h:105)
1   libsystem_kernel.dylib          0x2352d9f8 mach_msg + 40 (mach_msg.c:103)
2   CoreFoundation                  0x2386cf1c __CFRunLoopServiceMachPort + 136 (CFRunLoop.c:2345)
3   CoreFoundation                  0x2386b2a2 __CFRunLoopRun + 1050 (CFRunLoop.c:2607)
4   CoreFoundation                  0x237bdbb8 CFRunLoopRunSpecific + 516 (CFRunLoop.c:2814)
5   CoreFoundation                  0x237bd9ac CFRunLoopRunInMode + 108 (CFRunLoop.c:2844)
6   CFNetwork                       0x23e049e6 +[NSURLConnection(Loader) _resourceLoadLoop:] + 486 (NSURLConnection.mm:325)
7   Foundation                      0x240c632c __NSThread__start__ + 1144 (NSThread.m:1134)
8   libsystem_pthread.dylib         0x235e2c7e _pthread_body + 138 (pthread.c:656)
9   libsystem_pthread.dylib         0x235e2bf2 _pthread_start + 110 (pthread.c:692)
10  libsystem_pthread.dylib         0x235e0a08 thread_start + 8 (pthread_asm.s:162)

Thread 6 name:
Thread 6:
0   libsystem_kernel.dylib          0x23541f14 __select + 20
1   CoreFoundation                  0x238723c0 __CFSocketManager + 572 (CFSocket.c:2128)
2   libsystem_pthread.dylib         0x235e2c7e _pthread_body + 138 (pthread.c:656)
3   libsystem_pthread.dylib         0x235e2bf2 _pthread_start + 110 (pthread.c:692)
4   libsystem_pthread.dylib         0x235e0a08 thread_start + 8 (pthread_asm.s:162)

Thread 7:
0   libsystem_kernel.dylib          0x2354288c __workq_kernreturn + 8
1   libsystem_pthread.dylib         0x235e0e18 _pthread_wqthread + 1036 (pthread.c:1999)
2   libsystem_pthread.dylib         0x235e09fc start_wqthread + 8 (pthread_asm.s:147)

Thread 0 crashed with ARM Thread State (32-bit):
    r0: 0x00000000    r1: 0x00000000      r2: 0x39c940b0      r3: 0x00000000
    r4: 0x00000000    r5: 0x00631376      r6: 0x00000000      r7: 0x0040dcf4
    r8: 0x0064e984    r9: 0x00000000     r10: 0x00000001     r11: 0x16d54600
    ip: 0xf64d8965    sp: 0x0040db34      lr: 0x002011f0      pc: 0x002028ac
  cpsr: 0x60000010

The method where it crashes is:

public func paymentQueue(queue: SKPaymentQueue, updatedTransactions transactions: [SKPaymentTransaction]) {
  let delayTime = dispatch_time(DISPATCH_TIME_NOW, Int64(0.1 * Double(NSEC_PER_SEC)))
  dispatch_after(delayTime, dispatch_get_main_queue()) {

    for transaction in transactions {
      switch (transaction.transactionState) {
      case .Purchased:
        self.completeTransaction(transaction)
        break
      case .Failed:
        self.failedTransaction(transaction)
        break
      case .Restored:
        self.restoreTransaction(transaction)
        break
      case .Deferred:
        break
      case .Purchasing:
        break
      }
    }
  }
}

I used an IAPHelper.swift class which was taken from a Ray Wenderlich tutorial on IAPs: http://www.raywenderlich.com/105365/in-app-purchases-tutorial-getting-started

For the purposes of providing as much info as possible, I'll list below this class in its entirety & also relevant code from the UnlockGameViewController from which the option to unlock the game is presented:

//  IAPHelper.swift

import StoreKit

// ** NSNotifications - for sending messages to handle in UnlockGameVC ** //

/// Notification that is generated when a product is purchased.
public let IAPHelperProductPurchasedNotification = "IAPHelperProductPurchasedNotification"

/// Notification that is generated when a transaction fails.
public let IAPHelperTransactionFailedNotification = "IAPHelperTransactionFailedNotification"

/// Notification that is generated when cannot retrieve IAPs from iTunes.
public let IAPHelperConnectionErrorNotification = "IAPHelperConnectionErrorNotification"

/// Notification that is generated when we need to stop the spinner.
public let IAPHelperStopSpinnerNotification = "IAPHelperStopSpinnerNotification"

/// Product identifiers are unique strings registered on the app store.
public typealias ProductIdentifier = String

/// Completion handler called when products are fetched.
public typealias RequestProductsCompletionHandler = (success: Bool, products: [SKProduct]) -> ()


/// A Helper class for In-App-Purchases, it can fetch products, tell you if a product has been purchased,
/// purchase products, and restore purchases.  Uses NSUserDefaults to cache if a product has been purchased.
public class IAPHelper : NSObject  {

  /// MARK: - User facing API

  /// Initialize the helper.  Pass in the set of ProductIdentifiers supported by the app.
  public init(productIdentifiers: Set<ProductIdentifier>) {
    self.productIdentifiers = productIdentifiers

    for productIdentifier in productIdentifiers {
      let purchased = NSUserDefaults.standardUserDefaults().boolForKey(productIdentifier)
      if purchased {
        purchasedProductIdentifiers.insert(productIdentifier)
        print("Previously purchased: \(productIdentifier)")
      } else {
        print("Not purchased: \(productIdentifier)")
      }
    }

    super.init()

    SKPaymentQueue.defaultQueue().addTransactionObserver(self)
  }

  /// Gets the list of SKProducts from the Apple server and calls the handler with the list of products.
  public func requestProductsWithCompletionHandler(handler: RequestProductsCompletionHandler) {
    completionHandler = handler
    productsRequest = SKProductsRequest(productIdentifiers: productIdentifiers)
    productsRequest?.delegate = self
    productsRequest?.start()
  }

  /// Initiates purchase of a product.
  public func purchaseProduct(product: SKProduct) {
    print("Buying \(product.productIdentifier)...")
    let payment = SKPayment(product: product)
    SKPaymentQueue.defaultQueue().addPayment(payment)
  }

  /// Given the product identifier, returns true if that product has been purchased.
  public func isProductPurchased(productIdentifier: ProductIdentifier) -> Bool {
    return purchasedProductIdentifiers.contains(productIdentifier)
  }

  /// If the state of whether purchases have been made is lost (e.g. the
  /// user deletes and reinstalls the app) this will recover the purchases.
  public func restoreCompletedTransactions() {
    SKPaymentQueue.defaultQueue().restoreCompletedTransactions()
    print("Restoring...")
  }

  public func paymentQueueRestoreCompletedTransactionsFinished(queue: SKPaymentQueue) {
    print("Restore queue finished.")
    NSNotificationCenter.defaultCenter().postNotificationName(IAPHelperStopSpinnerNotification, object: nil)
  }

  public func paymentQueue(queue: SKPaymentQueue, restoreCompletedTransactionsFailedWithError error: NSError) {
    print("Restore queue failed.")
    NSNotificationCenter.defaultCenter().postNotificationName(IAPHelperConnectionErrorNotification, object: nil)
  }

  public class func canMakePayments() -> Bool {
    return SKPaymentQueue.canMakePayments()
  }

  /// MARK: - Private Properties

  // Used to keep track of the possible products and which ones have been purchased.
  private let productIdentifiers: Set<ProductIdentifier>
  private var purchasedProductIdentifiers = Set<ProductIdentifier>()

  // Used by SKProductsRequestDelegate
  private var productsRequest: SKProductsRequest?
  private var completionHandler: RequestProductsCompletionHandler?

}

// MARK: - SKProductsRequestDelegate

extension IAPHelper: SKProductsRequestDelegate {
  public func productsRequest(request: SKProductsRequest, didReceiveResponse response: SKProductsResponse) {
    print("Loaded list of products...")
    let products = response.products 
    completionHandler?(success: true, products: products)
    clearRequest()

    // debug printing
    for p in products {
      print("Found product: \(p.productIdentifier) \(p.localizedTitle) \(p.price.floatValue)")
    }
  }

  public func request(request: SKRequest, didFailWithError error: NSError) {
    print("Failed to load list of products.")
    print("Error: \(error)")
    NSNotificationCenter.defaultCenter().postNotificationName(IAPHelperConnectionErrorNotification, object: nil)
    clearRequest()
  }

  private func clearRequest() {
    productsRequest = nil
    completionHandler = nil
  }
}

extension IAPHelper: SKPaymentTransactionObserver {
  public func paymentQueue(queue: SKPaymentQueue, updatedTransactions transactions: [SKPaymentTransaction]) {
    let delayTime = dispatch_time(DISPATCH_TIME_NOW, Int64(0.1 * Double(NSEC_PER_SEC)))
    dispatch_after(delayTime, dispatch_get_main_queue()) {

      for transaction in transactions {
        switch (transaction.transactionState) {
        case .Purchased:
          print("case .Purchased:")
          self.completeTransaction(transaction)
          break
        case .Failed:
          print("case .Failed:")
          self.failedTransaction(transaction)
          break
        case .Restored:
          print("case .Restored:")
          self.restoreTransaction(transaction)
          break
        case .Deferred:
          print("case .Deferred:")
          break
        case .Purchasing:
          print("case .Purchasing:")
          break
        }
      }
    }
  }

  private func completeTransaction(transaction: SKPaymentTransaction) {
    print("completeTransaction...")
    provideContentForProductIdentifier(transaction.payment.productIdentifier)
    SKPaymentQueue.defaultQueue().finishTransaction(transaction)
  }

  private func restoreTransaction(transaction: SKPaymentTransaction) {
    let productIdentifier = transaction.originalTransaction!.payment.productIdentifier
    print("restoreTransaction... \(productIdentifier)")
    provideContentForProductIdentifier(productIdentifier)
    SKPaymentQueue.defaultQueue().finishTransaction(transaction)
  }

  // Helper: Saves the fact that the product has been purchased and posts a notification.
  private func provideContentForProductIdentifier(productIdentifier: String) {
    purchasedProductIdentifiers.insert(productIdentifier)
    NSUserDefaults.standardUserDefaults().setBool(true, forKey: productIdentifier)
    NSUserDefaults.standardUserDefaults().synchronize()
    NSNotificationCenter.defaultCenter().postNotificationName(IAPHelperProductPurchasedNotification, object: productIdentifier)
  }

  private func failedTransaction(transaction: SKPaymentTransaction) {
    print("failedTransaction...")
    NSNotificationCenter.defaultCenter().postNotificationName(IAPHelperStopSpinnerNotification, object: nil)
    if transaction.error!.code != SKErrorPaymentCancelled {
      print("Transaction error: \(transaction.error!.localizedDescription)")
      NSNotificationCenter.defaultCenter().postNotificationName(IAPHelperTransactionFailedNotification, object: nil)
    }
    SKPaymentQueue.defaultQueue().finishTransaction(transaction)
  }

}

Notification methods from UnlockGameViewController:

// MARK: - NSNotification methods

// When a product is purchased, this notification fires
func productPurchased(notification: NSNotification) {
  let productIdentifier = notification.object as! String
  for (index, product) in products.enumerate() {
    if product.productIdentifier == productIdentifier {
      // Only one IAP so we can assume this is the Unlock Full Game IAP
      activitySpinnerStop()

      agent.gameUnlocked = true  // sets the bool that the game has now been unlocked

      if openedFromMain == true {
        showAlertWith(Localization("GameUnlockedAlertTitle"), message: Localization("GameUnlockedAlertMessage"))
        noThanksButton.setTitle("Return to Main Menu", forState: UIControlState.Normal)
      } else {
        showAlertWith(Localization("GameUnlockedAlertTitle"), message: Localization("GameUnlockedAlertMessage2"))
      }
    }
  }
}

// When a transaction fails, this notification fires
func transactionFailed(notification: NSNotification) {
  activitySpinnerStop()
  showAlertWith(Localization("TransactionFailedAlertTitle"), message: Localization("TransactionFailedAlertMessage"))
}

// When we cannot connect to iTunes to retrieve the IAPs, this notification fires
func cannotConnect(notification: NSNotification) {
  activitySpinnerStop()
  showAlertWith(Localization("NoConnectionAlertTitle"), message: Localization("NoConnectionAlertMessage"))
}

There are two issues really: 1) Why does it crash in the first place? and 2) Why can't they delete the app, re-install and restore? Especially as the payment goes through.

To be totally honest, my users could live with the inital crash if they were able to delete, re-install and restore with no problem. One user has reported that after it crashed on his iPhone, he downloaded the game on his iPad and was able to restore the transaction just fine. So this leads me to believe that maybe it's an issue with corrupt data saved on the crashed device using NSUserDefaults? Something similar to this problem: iOS - strange crash on in App purchase restore function

This is my first app and I really have little experience in debugging. If a solution does not present itself, some guidance on how to debug this problem would be very welcome. I am stumped as to how to reproduce an issue on my device that only seems to happen when making an IAP on a live app.

Community
  • 1
  • 1
Eatton
  • 455
  • 4
  • 20
  • Why are you using dispatch_after? That sort of thing always sets my spidey sense tingling – Paulw11 Mar 08 '16 at 12:35
  • I've had this problem a while and asked about it before here. That was suggested as the answer and to be fair it has dramatically reduced the number of crashes: http://stackoverflow.com/questions/34289204/in-app-purchase-causes-occasional-crash – Eatton Mar 08 '16 at 12:37
  • Could it be that Game Center Authentication is hanging and crashing you entire app? "Breadcrumb Trail: (reverse chronological seconds) 14 GC Framework: startAuthenticationForExistingPrimaryPlayer" – smoothBlue Mar 31 '16 at 05:09

1 Answers1

1

I can't tell you what is wrong... all seams ok, just a little remark... maybe will be ok to have an extra "guard" in one of your method. Here is you method:

public func productsRequest(request: SKProductsRequest, didReceiveResponse response: SKProductsResponse) {
    print("Loaded list of products...")
    let products = response.products 
    completionHandler?(success: true, products: products)
    clearRequest()

    // debug printing
    for p in products {
      print("Found product: \(p.productIdentifier) \(p.localizedTitle) \(p.price.floatValue)")
    }
  }

Apple recommending to check if response.products is not nil if ([response.products count] > 0){...} https://developer.apple.com/library/ios/technotes/tn2387/_index.html

So.. will be better to have something like this..

    func productsRequest(request: SKProductsRequest!, didReceiveResponse response: SKProductsResponse!) {
        if response.products.count != 0 {
        .....
        }
    }

You can also make an extra check.. if in the response is not an invalidProductIdentifiers

. This case is more possible to happen in a real app, where you create and remove IAP products from time to time, and your app asks for product IDs that do not exist any longer... More here: http://www.appcoda.com/in-app-purchase-tutorial/

So.. in the above method you can also add

func productsRequest(request: SKProductsRequest!, didReceiveResponse response: SKProductsResponse!) {
    ...

    if response.invalidProductIdentifiers.count != 0 {
        println(response.invalidProductIdentifiers.description)
     //do something
    }
}
TonyMkenu
  • 7,597
  • 3
  • 27
  • 49
  • Thank you Tony, those links were very useful. It seems I am not implementing a couple of the best practices as detailed in the Apple doc you linked. I will go through it and make sure my app complies and will mark this answer as correct if it solves the problem. – Eatton Mar 10 '16 at 11:48