tbaldw02
tbaldw02

Reputation: 163

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!

Upvotes: 2

Views: 1110

Answers (3)

crashoverride777
crashoverride777

Reputation: 10674

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.default().remove(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.

Upvotes: 4

Anton Eregin
Anton Eregin

Reputation: 9099

Thanks to @crashoverride777 (!!!) Exactly the same issue was fixed after adding (Swift 4):

    override func viewDidDisappear(_ animated: Bool) {
       SKPaymentQueue.default().remove(self)
    }

Upvotes: 2

Fran
Fran

Reputation: 26

Thanks!, Worked for me too!!! The crash mentioned was happening for me too. The answer above is quite complete, however, with just the last comment it solved the Crash problem.

The code I used in Objective-C:

- (void) viewWillDisappear:(BOOL)animated
{
    [super viewWillDisappear:YES];
    [[SKPaymentQueue defaultQueue] removeTransactionObserver:self];
}

Upvotes: -1

Related Questions