Search code examples
iosswiftdelegatesin-app-purchaseapp-store

How to run a 'loading spinner' or similar while waiting for App Store to respond for in-app-purchases?


I'm currently offering in-app-purchases in a Swift based iOS app. I have a Store class which does the heavy lifting and have a StoreViewController which manages the view for the store. Currently when you press a buy button the response takes quite a while for the store (over 10 seconds minimum). This means that you could press the button repeatedly or give up on the purchase. I would like to change it so that when you press a button it disables the buttons and displays a spinner or similar until the buy process is ready to continue. In the code that I have already what would be the preferred way/place to do this?

Store class:

import Foundation
import StoreKit

protocol ClassStoreDelegate: class {
    func storeUpdateReceived(store: Store)
}

class Store: NSObject, SKProductsRequestDelegate, SKPaymentTransactionObserver {
    // Properties
    weak var delegate: ClassStoreDelegate?
    var list = [SKProduct]()
    var p = SKProduct()
    var localPrice: String?
    var presentAlert = false
    var alertTitle = "Alert"
    var alertMessage = "Unfortunately something went wrong"
    var purchaseSuccess = false

    // Methods
    // Calling the delegate method
    func storeUpdate() {
        delegate?.storeUpdateReceived(store: self)
    }

    // Buy the product
    func buy(productId: String) {
        var foundProduct = false
        var counter = 0
        while counter < list.count && !foundProduct {
            if (list[counter].productIdentifier == productId){
                p = list[counter]
                foundProduct = true
            }
            counter = counter + 1
        }
        buyProduct()
    }

    func buyProduct() {
        let pay = SKPayment(product: p)
        SKPaymentQueue.default().add(self)
        SKPaymentQueue.default().add(pay as SKPayment)
    }

    func paymentQueue(_ queue: SKPaymentQueue, updatedTransactions transactions: [SKPaymentTransaction]) {
        for transaction: AnyObject in transactions {
            let trans = transaction as! SKPaymentTransaction
            switch trans.transactionState {
            case .purchased:
                let prodID = p.productIdentifier
                switch prodID {
                case "volume2":
                    VolumeTableViewController.saveUnlocked(volumeUnlocked: 1)
                    purchaseSuccess = true
                    delegate?.storeUpdateReceived(store: self)
                case "volume3":
                    VolumeTableViewController.saveUnlocked(volumeUnlocked: 2)
                    purchaseSuccess = true
                    delegate?.storeUpdateReceived(store: self)
                case "volume4":
                    VolumeTableViewController.saveUnlocked(volumeUnlocked: 3)
                    purchaseSuccess = true
                    delegate?.storeUpdateReceived(store: self)
                case "volume5":
                    VolumeTableViewController.saveUnlocked(volumeUnlocked: 4)
                    purchaseSuccess = true
                    delegate?.storeUpdateReceived(store: self)
                case "all":
                    for i in 0...VolumeTableViewController.unlocked.count-1 {
                        VolumeTableViewController.saveUnlocked(volumeUnlocked: i)
                    }
                    purchaseSuccess = true
                    delegate?.storeUpdateReceived(store: self)
                default:
                    delegate?.storeUpdateReceived(store: self)
                }
                queue.finishTransaction(trans)
                break
            case .failed:
                alertTitle = "Something went wrong"
                alertMessage = "Unfortunately the purchase failed"
                presentAlert = true
                delegate?.storeUpdateReceived(store: self)
                queue.finishTransaction(trans)
                break
            case .restored:
                SKPaymentQueue.default().finishTransaction(transaction as! SKPaymentTransaction)
                // Try to pop back after successful restore
                purchaseSuccess = true
                delegate?.storeUpdateReceived(store: self)
                queue.finishTransaction(trans)
                break
            default:
                break
            }
        }
    }
    // Restore products
    func restore() {
        SKPaymentQueue.default().add(self)
        SKPaymentQueue.default().restoreCompletedTransactions()
    }

    func getProducts() {
        if(SKPaymentQueue.canMakePayments()) {
            let productID: NSSet = NSSet(objects: "volume2", "volume3", "volume4", "volume5", "all")
            let request: SKProductsRequest = SKProductsRequest(productIdentifiers: productID as! Set<String>)
            request.delegate = self
            request.start()
        } else {
            alertTitle = "Something went wrong"
            alertMessage = "Your phone does not appear to be set up for making purchases"
            presentAlert = true
            delegate?.storeUpdateReceived(store: self)
        }
    }

    func productsRequest(_ request: SKProductsRequest, didReceive response: SKProductsResponse) {
        let myProduct = response.products
        for product in myProduct {
            list.append(product)
        }
        delegate?.storeUpdateReceived(store: self)
    }

    func paymentQueueRestoreCompletedTransactionsFinished(_ queue: SKPaymentQueue) {
        let transactionsArray = queue.transactions
        if (transactionsArray.isEmpty) {
            alertTitle = "Something went wrong"
            alertMessage = "We weren't able to find any previous purchases for your account"
            presentAlert = true
            delegate?.storeUpdateReceived(store: self)
        }
        else {
            for transaction in transactionsArray {
                let t: SKPaymentTransaction = transaction
                let prodID = t.payment.productIdentifier as String
                switch prodID {
                case "volume2":
                    VolumeTableViewController.saveUnlocked(volumeUnlocked: 1)
                    delegate?.storeUpdateReceived(store: self)
                case "volume3":
                    VolumeTableViewController.saveUnlocked(volumeUnlocked: 2)
                    delegate?.storeUpdateReceived(store: self)
                case "volume4":
                    VolumeTableViewController.saveUnlocked(volumeUnlocked: 3)
                    delegate?.storeUpdateReceived(store: self)
                case "volume5":
                    VolumeTableViewController.saveUnlocked(volumeUnlocked: 4)
                    delegate?.storeUpdateReceived(store: self)
                case "all":
                    for i in 0...VolumeTableViewController.unlocked.count-1 {
                        VolumeTableViewController.saveUnlocked(volumeUnlocked: i)
                    }
                    delegate?.storeUpdateReceived(store: self)
                default:
                    alertTitle = "Something went wrong"
                    alertMessage = "We weren't able to find the correct product"
                    presentAlert = true
                    delegate?.storeUpdateReceived(store: self)
                }
            }
        }
    }
    // Format the price and display
    func formatPrice(price: NSDecimalNumber) -> String {
        let formatter = NumberFormatter()
        formatter.locale = Locale.current
        formatter.numberStyle = .currency
        if let formattedPrice = formatter.string(from: price){
            localPrice = (" \(formattedPrice)")
        }
        return localPrice!
    }
}

StoreViewController class:

let store = Store() // Global store instance

import UIKit

class StoreViewController: UIViewController, ClassStoreDelegate {
    // Properties
    /*  let alert = UIAlertController(title: "Something went wrong", message:
     "Unfortunately, something went wrong with your request", preferredStyle: UIAlertControllerStyle.alert) */
    // Actions
    @IBAction func btn2(_ sender: UIButton) {
        store.buy(productId: store.list[0].productIdentifier)
    }

    @IBAction func btn3(_ sender: UIButton) {
        store.buy(productId: store.list[1].productIdentifier)
    }

    @IBAction func btn4(_ sender: UIButton) {
        store.buy(productId: store.list[2].productIdentifier)
    }

    @IBAction func btn5(_ sender: UIButton) {
        store.buy(productId: store.list[3].productIdentifier)
    }

    @IBAction func btnAll(_ sender: UIButton) {
        store.buy(productId: store.list[4].productIdentifier)
    }

    @IBAction func btnRestore(_ sender: UIButton) {
        store.restore()
    }
    // Outlets
    @IBOutlet weak var btn2: UIButton!
    @IBOutlet weak var btn3: UIButton!
    @IBOutlet weak var btn4: UIButton!
    @IBOutlet weak var btn5: UIButton!
    @IBOutlet weak var btnAll: UIButton!
    @IBOutlet weak var btnRestore: UIButton!

    override func viewDidLoad() {
        super.viewDidLoad()
        // Disable buttons until prices loaded
        btn2.isEnabled = false
        btn3.isEnabled = false
        btn4.isEnabled = false
        btn5.isEnabled = false
        btnAll.isEnabled = false
        btnRestore.isEnabled = false

        store.delegate = self // bind the delegate like this?
        //      alert.addAction(UIAlertAction(title: "OK", style: UIAlertActionStyle.default)) // Set up the action for the alert button

        // Get list of products for the store
        self.navigationItem.title = "Store"
        // update once the list of products is got from the store object
        store.getProducts()
    }

    // Running the delegate update
    func storeUpdateReceived(store: Store) {
        if ((store.list.count) > 0) {
                        btnRestore.setTitle("Restore", for: .normal)

            if store.list[0].productIdentifier == "all" {
                                btn2.setTitle(store.list[0].localizedTitle + " " + store.formatPrice(price: store.list[0].price), for: .normal)
            }
            if store.list[1].productIdentifier == "volume2" {
                if VolumeTableViewController.unlocked[1] {
                    btn3.isHidden = true
                } else {
                    btn3.setTitle(store.list[1].localizedTitle + " " + store.formatPrice(price: store.list[1].price), for: .normal)
                }
            }
                        if store.list[2].productIdentifier == "volume3" {
                if VolumeTableViewController.unlocked[2] {
                    btn4.isHidden = true
                } else {
                    btn4.setTitle(store.list[2].localizedTitle + " " + store.formatPrice(price: store.list[2].price), for: .normal)
                }
            }
            if store.list[3].productIdentifier  == "volume4" {
                if VolumeTableViewController.unlocked[3] {
                    btn5.isHidden = true
                } else {
                    btn5.setTitle(store.list[3].localizedTitle + " " + store.formatPrice(price: store.list[3].price), for: .normal)
                }
            }
            if store.list[4].productIdentifier == "volume5" {
                if VolumeTableViewController.unlocked[4] {
                    btnAll.isHidden = true
                } else {
                    btnAll.setTitle(store.list[4].localizedTitle + " " + store.formatPrice(price: store.list[4].price), for: .normal)
                }
            }

            // Now enable the buttons
            btn2.isEnabled = true
            btn3.isEnabled = true
            btn4.isEnabled = true
            btn5.isEnabled = true
            btnAll.isEnabled = true
            btnRestore.isEnabled = true
        }
        if store.purchaseSuccess {
        performSegueToReturnBack()
            store.purchaseSuccess = false
        }
    }

    // method to go back when complete
    func performSegueToReturnBack()  {
        if let nav = self.navigationController {
            nav.popViewController(animated: true)
        } else {
            self.dismiss(animated: true, completion: nil)
        }
    }

    override func didReceiveMemoryWarning() {
        super.didReceiveMemoryWarning()
        // Dispose of any resources that can be recreated.
    }
}

Solution

  • I would like to change it so that when you press a button it disables the buttons

    Then disable the buttons or hide the buy interface, in immediate response to the tap on the Buy button.

    and displays a spinner or similar until the buy process is ready to continue

    You can't. You have no way to know what's happening. The "buy process" is an interchange outside your app, between the user and the runtime. You are not informed of what's going on. If the user cancels the purchase, or if there is any problem with the user's password or store account, you won't get any event informing you of what's happened. So if you put up some special "waiting" interface, it might remain forever.

    Basically, the title of your question reveals a deep misconception. You do not "wait". You just go on with the life of your app. The purchase process is asynchronous and it is out of process for your app. When and if the payment queue tells your app that something has happened, you respond; that's all.

    (See Apple's guidance about this; as they point out, under some circumstances the message from the payment queue might not arrive for days! You'd look pretty silly "waiting" for that.)

    So the correct procedure is what we said first. If the user taps Buy, disable or take down the purchase interface, and that's all.