tbaldw02 tbaldw02 - 1 month ago 18
iOS Question

Crash when restoring purchases in swift

I'm running into some confusing behavior with my in-app purchases restore feature. Currently, I have the restore feature linked to a button, it seems to crash when I activate it multiple times. For example, if I hit it, restore, navigate to another view, then back to hit the restore again it will crash.

Can anyone check my code and see if I'm missing something staring me in the face?

import SpriteKit
import StoreKit

class PurchaseView: SKScene, SKPaymentTransactionObserver, SKProductsRequestDelegate{

var instructLabel = SKLabelNode()
var priceLabel = SKLabelNode()

var saleBadgeIcon = SKSpriteNode()
var backIcon = SKSpriteNode()
var restoreIcon = SKSpriteNode()

var blueDiceDemo = SKSpriteNode()
var redDiceDemo = SKSpriteNode()
var greenDiceDemo = SKSpriteNode()
var grayDiceDemo = SKSpriteNode()

var bluePID: String = "dice.blue.add"
var redPID: String = "dice.red.add"
var greenPID: String = "dice.green.add"
var grayPID: String = "dice.gray.add"

private var request : SKProductsRequest!
private var products : [SKProduct] = []

private var blueDicePurchased : Bool = false
private var redDicePurchased : Bool = false
private var greenDicePurchased : Bool = false
private var grayDicePurchased : Bool = false

override func didMoveToView(view: SKView) {
// In-App Purchase
initInAppPurchases()

/*
checkAndActivateGreenColor()
checkAndActivateRedColor()
checkAndActivateGrayColor()
checkAndActivateBlueColor()
*/

createInstructionLabel()
createBackIcon()
createRestoreIcon()
createBlueDicePurchase()
createRedDicePurchase()
createGreenDicePurchase()
createGrayDicePurchase()

checkAndActivateDiceColor(bluePID)
checkAndActivateDiceColor(redPID)
checkAndActivateDiceColor(greenPID)
checkAndActivateDiceColor(grayPID)
}

override func touchesBegan(touches: Set<UITouch>, withEvent event: UIEvent?) {
for touch in touches {
let location = touch.locationInNode(self)
let node = nodeAtPoint(location)

if (node == backIcon) {
let gameScene = GameScene(size: self.size)
let transition = SKTransition.doorsCloseVerticalWithDuration(0.5)
gameScene.scaleMode = SKSceneScaleMode.ResizeFill
gameScene.backgroundColor = SKColor.whiteColor()
self.scene!.view?.presentScene(gameScene, transition: transition)
} else if (node == restoreIcon) {
print("restore my purchases")

let alert = UIAlertController(title: "Restore Purchases", message: "", preferredStyle: UIAlertControllerStyle.Alert)

alert.addAction(UIAlertAction(title: "Restore", style: UIAlertActionStyle.Default) { _ in
self.restorePurchasedProducts()
})

alert.addAction(UIAlertAction(title: "Cancel", style: UIAlertActionStyle.Default) { _ in

})

// Show the alert
self.view?.window?.rootViewController?.presentViewController(alert, animated: true, completion: nil)

//restorePurchasedProducts()
} else if (node == blueDiceDemo) {
print("buy blue")
if (!blueDicePurchased) {
inAppPurchase(blueDicePurchased, pid: bluePID)
}
} else if (node == redDiceDemo) {
print("buy red")
if (!redDicePurchased) {
inAppPurchase(redDicePurchased, pid: redPID)
}
} else if (node == greenDiceDemo) {
print("buy green")
if (!greenDicePurchased) {
inAppPurchase(greenDicePurchased, pid: greenPID)
}
} else if (node == grayDiceDemo) {
print("buy gray")
if (!grayDicePurchased) {
inAppPurchase(grayDicePurchased, pid: grayPID)
}
}
}
}

func createBlueDicePurchase() {
blueDiceDemo = SKSpriteNode(imageNamed: "dice1_blue")
blueDiceDemo.setScale(0.6)
blueDiceDemo.position = CGPoint(x: CGRectGetMidX(self.frame) + blueDiceDemo.size.width * 2, y: CGRectGetMidY(self.frame))
addChild(blueDiceDemo)

createSaleBadge(blueDiceDemo)
}

func createGrayDicePurchase() {
grayDiceDemo = SKSpriteNode(imageNamed: "dice1_gray")
grayDiceDemo.setScale(0.6)
grayDiceDemo.position = CGPoint(x: CGRectGetMidX(self.frame), y: CGRectGetMidY(self.frame))
addChild(grayDiceDemo)

createSaleBadge(grayDiceDemo)
}

func createRedDicePurchase() {
redDiceDemo = SKSpriteNode(imageNamed: "dice1_red")
redDiceDemo.setScale(0.6)
redDiceDemo.position = CGPoint(x: CGRectGetMidX(self.frame) - blueDiceDemo.size.width * 2, y: CGRectGetMidY(self.frame))
addChild(redDiceDemo)

createSaleBadge(redDiceDemo)
}

func createGreenDicePurchase() {
greenDiceDemo = SKSpriteNode(imageNamed: "dice1_green")
greenDiceDemo.setScale(0.6)
greenDiceDemo.position = CGPoint(x: CGRectGetMidX(self.frame), y: CGRectGetMidY(self.frame) - blueDiceDemo.size.height * 1.5)
addChild(greenDiceDemo)

createSaleBadge(greenDiceDemo)
}

func createInstructionLabel() {
instructLabel = SKLabelNode(fontNamed: "Helvetica")
instructLabel.text = "Click item to purchase!"
instructLabel.fontSize = 24
instructLabel.fontColor = SKColor.blackColor()
instructLabel.position = CGPoint(x: CGRectGetMidX(self.frame), y: CGRectGetMaxY(self.frame) - 50)
addChild(instructLabel)
}

func createPurchasedLabel(node: SKSpriteNode) {
let purchasedLabel = SKLabelNode(fontNamed: "Helvetica")
purchasedLabel.text = "purchased"
purchasedLabel.fontSize = 30
purchasedLabel.zPosition = 2
purchasedLabel.fontColor = SKColor.blackColor()
purchasedLabel.position = CGPoint(x: 0, y: -7.5)
node.addChild(purchasedLabel)
}

func createRestoreIcon() {
restoreIcon = SKSpriteNode(imageNamed: "download")
restoreIcon.setScale(0.4)
restoreIcon.position = CGPoint(x: CGRectGetMinX(self.frame) + 30, y: CGRectGetMinY(self.frame) + 30)
addChild(restoreIcon)
}

func createBackIcon() {
backIcon = SKSpriteNode(imageNamed: "remove")
backIcon.setScale(0.5)
backIcon.position = CGPoint(x: CGRectGetMaxX(self.frame) - 30, y: CGRectGetMinY(self.frame) + 30)
addChild(backIcon)
}

func createSaleBadge(node: SKSpriteNode) {
saleBadgeIcon = SKSpriteNode(imageNamed: "badge")
saleBadgeIcon.setScale(0.4)
saleBadgeIcon.zPosition = 2
saleBadgeIcon.position = CGPoint(x: node.size.width/2, y: node.size.height/2)
node.addChild(saleBadgeIcon)
}

func inAppPurchase(dicePurchased: Bool, pid: String) {
let alert = UIAlertController(title: "In-App Purchases", message: "", preferredStyle: UIAlertControllerStyle.Alert)

// Add an alert action for each available product
for (var i = 0; i < products.count; i++) {
let currentProduct = products[i]
if (currentProduct.productIdentifier == pid && !dicePurchased) {
// Get the localized price
let numberFormatter = NSNumberFormatter()
numberFormatter.numberStyle = .CurrencyStyle
numberFormatter.locale = currentProduct.priceLocale
// Add the alert action
alert.addAction(UIAlertAction(title: currentProduct.localizedTitle + " " + numberFormatter.stringFromNumber(currentProduct.price)!, style: UIAlertActionStyle.Default) { _ in
// Perform the purchase
self.buyProduct(currentProduct)
})

alert.addAction(UIAlertAction(title: "Cancel", style: UIAlertActionStyle.Default) { _ in

})

// Show the alert
self.view?.window?.rootViewController?.presentViewController(alert, animated: true, completion: nil)
}
}
}

//Initializes the App Purchases
func initInAppPurchases() {
SKPaymentQueue.defaultQueue().addTransactionObserver(self)
// Get the list of possible purchases
if self.request == nil {
self.request = SKProductsRequest(productIdentifiers: Set(["dice.green.add", "dice.blue.add", "dice.gray.add","dice.red.add"]))
self.request.delegate = self
self.request.start()
}
}

// Request a purchase
func buyProduct(product: SKProduct) {
let payment = SKPayment(product: product)
SKPaymentQueue.defaultQueue().addPayment(payment)
}

// Restore purchases
func restorePurchasedProducts() {
SKPaymentQueue.defaultQueue().restoreCompletedTransactions()
}

// StoreKit protocoll method. Called when the AppStore responds
func productsRequest(request: SKProductsRequest, didReceiveResponse response: SKProductsResponse) {
self.products = response.products
self.request = nil
}

// StoreKit protocoll method. Called when an error happens in the communication with the AppStore
func request(request: SKRequest, didFailWithError error: NSError) {
print(error)
self.request = nil
}

// StoreKit protocoll method. Called after the purchase
func paymentQueue(queue: SKPaymentQueue, updatedTransactions transactions: [SKPaymentTransaction]) {
for transaction in transactions {
switch (transaction.transactionState) {
case .Purchased:
if transaction.payment.productIdentifier == "dice.green.add" {
handleDiceColorPurchase(greenPID)
print("buying green")
} else if transaction.payment.productIdentifier == "dice.blue.add" {
handleDiceColorPurchase(bluePID)
print("buying blue")
} else if transaction.payment.productIdentifier == "dice.red.add" {
handleDiceColorPurchase(redPID)
print("buying red")
} else if transaction.payment.productIdentifier == "dice.gray.add" {
handleDiceColorPurchase(grayPID)
print("buying gray")
} else {
print("Error: Invalid Product ID")
}
queue.finishTransaction(transaction)
case .Restored:
if transaction.payment.productIdentifier == "dice.green.add" {
handleDiceColorPurchase(greenPID)
print("restoring green")
} else if transaction.payment.productIdentifier == "dice.blue.add" {
handleDiceColorPurchase(bluePID)
print("restoring blue")
} else if transaction.payment.productIdentifier == "dice.red.add" {
handleDiceColorPurchase(redPID)
print("restoring red")
} else if transaction.payment.productIdentifier == "dice.gray.add" {
handleDiceColorPurchase(grayPID)
print("restoring gray")
} else {
print("Error: Invalid Product ID")
}
queue.finishTransaction(transaction)
case .Failed:
print("Payment Error: \(transaction.error)")
queue.finishTransaction(transaction)
default:
print("Transaction State: \(transaction.transactionState)")
}
}
}

// Called after the purchase to provide the colored dice feature
func handleDiceColorPurchase(pid: String){
switch(pid) {
case greenPID:
greenDicePurchased = true
greenDiceDemo.alpha = 0.25
greenDiceDemo.removeAllChildren()
createPurchasedLabel(greenDiceDemo)
case redPID:
redDicePurchased = true
redDiceDemo.alpha = 0.25
redDiceDemo.removeAllChildren()
createPurchasedLabel(redDiceDemo)
case grayPID:
grayDicePurchased = true
grayDiceDemo.alpha = 0.25
grayDiceDemo.removeAllChildren()
createPurchasedLabel(grayDiceDemo)
case bluePID:
blueDicePurchased = true
blueDiceDemo.alpha = 0.25
blueDiceDemo.removeAllChildren()
createPurchasedLabel(blueDiceDemo)
default:
print("No action taken, incorrect PID")
}

checkAndActivateDiceColor(pid)
// persist the purchase locally
NSUserDefaults.standardUserDefaults().setBool(true, forKey: pid)
}

func checkAndActivateDiceColor(pid: String){
if NSUserDefaults.standardUserDefaults().boolForKey(pid) {
switch(pid) {
case greenPID:
greenDicePurchased = true
greenDiceDemo.alpha = 0.25
greenDiceDemo.removeAllChildren()
createPurchasedLabel(greenDiceDemo)
case redPID:
redDicePurchased = true
redDiceDemo.alpha = 0.25
redDiceDemo.removeAllChildren()
createPurchasedLabel(redDiceDemo)
case grayPID:
grayDicePurchased = true
grayDiceDemo.alpha = 0.25
grayDiceDemo.removeAllChildren()
createPurchasedLabel(grayDiceDemo)
case bluePID:
blueDicePurchased = true
blueDiceDemo.alpha = 0.25
blueDiceDemo.removeAllChildren()
createPurchasedLabel(blueDiceDemo)
default:
print("No action taken, incorrect PID")
}
}
}


}

When it crashes, there isn't much info that I can decipher. I get an error stating EXC_BAD_ACCESS (code=1, address=0xc) on my AppDelegate class and something highlighted green stating Enqueued from com.apple.root.default-qos.overcommit(Thread 4)

Any help is appreciated!

Answer

Your code is bit messy, lets go through it

1) Put your NSUserDefaults keys and product IDs into a struct above your class so you avoid typos.

 struct ProductID {
    static let diceGrayAdd = "dice.gray.add"
    ....
  }

and get it like so

....payment.productIdentifier == ProductID.diceGrayAdd {     

2) You are not checking if payments can actually be made before requesting products.

 guard SKPaymentQueue.canMakePayments() else {
   // show alert that IAPs are not enabled
   return
 }

3) Why are you setting the requests to nil in the delegate methods? That makes no sense. Delete all these lines in your code

self.request = nil

4) You should also use originalTransaction in the .Restore case, your way is not quite correct. Unfortunately loads of tutorials dont teach you this.

 case .Restored:

 /// Its an optional so safely unwrap it first
 if let originalTransaction = transaction.originalTransaction {              

     if originalTransaction.payment.productIdentifier == ProductID.diceGrayAdd {
           handleDiceColorPurchase(greenPID)
           print("restoring green")
       }
       ....
   }

You could also make your code a bit cleaner by putting the unlocking action into another function, so you dont have to write duplicate code in the .Purchased and .Restored cases.

Check my answer I posted recently for this. You should also handle the errors in the .Failed case.

Restore Purchase : Non-Consumable

5) Also when you transition away from the shop you should call

requests.cancel()

to make sure you don't change viewController in the middle of a request. In my spriteKit games that causes me to get a crash, so its good to put it in there to make sure its cancelled.

6) Are you calling this line

  SKPaymentQueue.defaultQueue().removeTransactionObserver(self)

This should get called when you close your app or in your case probably when you exit the shop. This make sure all transactions are removed from the observer and won't show up in the future in form of login message.

Let me know if this fixes your crashes.