首页 > 解决方案 > 消耗品 IAP 偶尔不会提供奖励,并且消耗品促销代码根本不起作用

问题描述

我有一个应用程序提供硬币作为消耗性 IAP。大多数情况下这工作正常,但偶尔会有用户联系我说虽然付款已通过并且他收到了 Apple 的发票,但他没有收到他的硬币。

此外,该应用程序根本不处理兑换消耗品促销代码。兑换时,App Store 会显示 IAP 已成功兑换的消息,但在打开应用程序时,硬币永远不会出现。

我遵循了 Apple 的建议,并在 App Delegate 文件中添加了事务观察器,但这个问题仍然存在。这让我发疯,所以如果任何熟悉消耗性 IAP 的人可以查看我的代码并帮助我,我将永远感激不尽。

请注意:我已尝试修改本教程https://www.raywenderlich.com/1145-in-app-purchases-tutorial-consumables,它没有在 App Delegate 文件中添加事务观察器,但我认为我已经一路上错过了一个技巧。

AppDelegate.swift(仅显示相关代码):

class AppDelegate: UIResponder, UIApplicationDelegate {

  let iapHelper = IAPHelper()

  func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
    SKPaymentQueue.default().add(iapHelper) // adds the transaction observer
  }

  func applicationWillTerminate(_ application: UIApplication) {
    SKPaymentQueue.default().remove(iapHelper) // remove the transaction observer
  }
}

IAPHelper.swift: 不确定我是否正确地做事static var helper = IAPHelper()

import StoreKit

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

// 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 ProductsRequestCompletionHandler = (_ success: Bool, _ products: [SKProduct]?) -> ()


open class IAPHelper : NSObject  {

    /// MARK: - User facing API

    fileprivate let productIdentifiers: Set<ProductIdentifier>
    fileprivate var purchasedProductIdentifiers = Set<ProductIdentifier>()

    fileprivate var productsRequest: SKProductsRequest?
    fileprivate var productsRequestCompletionHandler: ProductsRequestCompletionHandler?

    static var helper = IAPHelper()  // is this right??

    /// Init method
    override init() {
        // Set up the list of productIdentifiers
        let PackOf4000Coins =  "com.xxx.xxx.4000Coins"   // This is the ProductID in iTunes Connect
        let PackOf10000Coins =  "com.xxx.xxx.10000Coins"
        let PackOf30000Coins =  "com.xxx.xxx.30000Coins"
        let PackOf75000Coins =  "com.xxx.xxx.75000Coins"
        let PackOf175000Coins = "com.xxx.xxx.175000Coins"
        let PackOf500000Coins = "com.xxx.xxx.500000Coins"
        let RemoveAds =  "com.xxx.xxx.RemoveAds" 
        let PlayerEditor =  "com.xxx.xxx.PlayerEditor" 

        self.productIdentifiers = [PackOf4000Coins, PackOf10000Coins, PackOf30000Coins, PackOf75000Coins, PackOf175000Coins, PackOf500000Coins, RemoveAds, PlayerEditor]

        for productIdentifier in self.productIdentifiers {
            let purchased = UserDefaults.standard.bool(forKey: productIdentifier)
            if purchased {
                purchasedProductIdentifiers.insert(productIdentifier)
                print("Previously purchased: \(productIdentifier)")
            } else {
                print("Not purchased: \(productIdentifier)")
            }
        }

        super.init()

    }

}

// MARK: - StoreKit API

extension IAPHelper {

    public func requestProducts(_ completionHandler: @escaping ProductsRequestCompletionHandler) {
        productsRequest?.cancel()
        productsRequestCompletionHandler = completionHandler

        productsRequest = SKProductsRequest(productIdentifiers: productIdentifiers)
        productsRequest!.delegate = self
        productsRequest!.start()
    }

    public func buyProduct(_ product: SKProduct) {
        print("Buying \(product.productIdentifier)...")
        let payment = SKPayment(product: product)
        SKPaymentQueue.default().add(payment)
    }

    public func isProductPurchased(_ productIdentifier: ProductIdentifier) -> Bool {
        return purchasedProductIdentifiers.contains(productIdentifier)
    }

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

    public func restorePurchases() {
        SKPaymentQueue.default().restoreCompletedTransactions()
    }

    public func paymentQueueRestoreCompletedTransactionsFinished(_ queue: SKPaymentQueue) {
        print("Restore queue finished.")
        NotificationCenter.default.post(name: Notification.Name(rawValue: IAPHelperStopSpinnerNotification), object: nil)
    }

    public func paymentQueue(_ queue: SKPaymentQueue, restoreCompletedTransactionsFailedWithError error: Error) {
        print("Restore queue failed.")
        NotificationCenter.default.post(name: Notification.Name(rawValue: IAPHelperConnectionErrorNotification), object: nil)
    }

}

// MARK: - SKProductsRequestDelegate

extension IAPHelper: SKProductsRequestDelegate {
    public func productsRequest(_ request: SKProductsRequest, didReceive response: SKProductsResponse) {
        print("Loaded list of products...")
        let products = response.products
        productsRequestCompletionHandler?(true, products)
        clearRequestAndHandler()
    }

    public func request(_ request: SKRequest, didFailWithError error: Error) {
        print("Failed to load list of products.")
        print("Error: \(error.localizedDescription)")
        productsRequestCompletionHandler?(false, nil)
        NotificationCenter.default.post(name: Notification.Name(rawValue: IAPHelperConnectionErrorNotification), object: nil)
        clearRequestAndHandler()
    }

    fileprivate func clearRequestAndHandler() {
        productsRequest = nil
        productsRequestCompletionHandler = nil
    }
}

// MARK: - SKPaymentTransactionObserver

extension IAPHelper: SKPaymentTransactionObserver {

    public func paymentQueue(_ queue: SKPaymentQueue, updatedTransactions transactions: [SKPaymentTransaction]) {
        for transaction in transactions {
            switch (transaction.transactionState) {
            case .purchased:
                completeTransaction(transaction)
                break
            case .failed:
                failedTransaction(transaction)
                break
            case .restored:
                restoreTransaction(transaction)
                break
            case .deferred:
                break
            case .purchasing:
                break
            }
        }
    }

    fileprivate func completeTransaction(_ transaction: SKPaymentTransaction) {
        print("completeTransaction...")
        deliverPurchaseNotificationForIdentifier(transaction.payment.productIdentifier)
        SKPaymentQueue.default().finishTransaction(transaction)
    }

    fileprivate func restoreTransaction(_ transaction: SKPaymentTransaction) {
        guard let productIdentifier = transaction.original?.payment.productIdentifier else { return }

        print("restoreTransaction... \(productIdentifier)")
        deliverPurchaseNotificationForIdentifier(productIdentifier)
        SKPaymentQueue.default().finishTransaction(transaction)
    }

    fileprivate func failedTransaction(_ transaction: SKPaymentTransaction) {
        print("failedTransaction...")
        NotificationCenter.default.post(name: Notification.Name(rawValue: IAPHelperStopSpinnerNotification), object: nil)
        if transaction.error!._code != SKError.paymentCancelled.rawValue {
            print("Transaction Error: \(String(describing: transaction.error?.localizedDescription))")
            NotificationCenter.default.post(name: Notification.Name(rawValue: IAPHelperTransactionFailedNotification), object: nil)
        } else {
            print("Transaction Error else statement")
        }

        SKPaymentQueue.default().finishTransaction(transaction)
        NotificationCenter.default.post(name: Notification.Name(rawValue: IAPHelperTransactionFailedNotification), object: nil)

    }

    fileprivate func deliverPurchaseNotificationForIdentifier(_ identifier: String?) {
        guard let identifier = identifier else { return }

        purchasedProductIdentifiers.insert(identifier)
        UserDefaults.standard.set(true, forKey: identifier)
        UserDefaults.standard.synchronize()
        NotificationCenter.default.post(name: Notification.Name(rawValue: IAPHelperPurchaseNotification), object: identifier)
    }
}

GameStoreViewController.swift:(我担心我没有从这个 VC 正确访问 IAPHelper,这似乎是错误的)

import UIKit    
import StoreKit    

class GameStoreViewController: UIViewController, GetMoneyViewControllerDelegate {    

// A list of available IAPs    
var products = [SKProduct]()  // non-consumables    
var _coinProducts = [SKProduct]() // consumables   

override func viewDidLoad() {    
    super.viewDidLoad()    

    retrieveIAPs()  // Fetch the products from iTunes connect    

    // ** NSNotifications - this is how IAPHelper sends messages for me to handle here ** //    

    // Subscribe to a notification that fires when a product is purchased.    
    NotificationCenter.default.addObserver(self, selector: #selector(GameStoreViewController.productPurchased(_:)), name: NSNotification.Name(rawValue: IAPHelperPurchaseNotification), object: nil)    

    // Subscribe to a notification that fires when a transaction fails.    
    NotificationCenter.default.addObserver(self, selector: #selector(GameStoreViewController.transactionFailed(_:)), name: NSNotification.Name(rawValue: IAPHelperTransactionFailedNotification), object: nil)    

    // Subscribe to a notification that fires when there's a connection error.    
    NotificationCenter.default.addObserver(self, selector: #selector(GameStoreViewController.cannotConnect(_:)), name: NSNotification.Name(rawValue: IAPHelperConnectionErrorNotification), object: nil)    

    // Subscribe to a notification that fires when the activity spinner needs to stop.    
    NotificationCenter.default.addObserver(self, selector: #selector(GameStoreViewController.stopSpinner(_:)), name: NSNotification.Name(rawValue: IAPHelperStopSpinnerNotification), object: nil)    

}    

// Fetch the products from iTunes connect, put into the products array    
func retrieveIAPs() {    
    products = []    
    IAPHelper.helper.requestProducts { success, products in    
        if success {    
            self.products = products!    
            print("Success. Products are: \(self.products)")    
            self._IAPsHaveBeenRetrieved = true    
            self.activitySpinnerStop()    

            // * Set up _coinProducts * //    

            // Filter out 'Other' IAPs    
            self._coinProducts = self.products.filter { $0.productIdentifier != "com.xxx.xxx.PlayerEditor" && $0.productIdentifier != "com.xxx.xxx.RemoveAds" }    

        } else {    
            print("Unable to connect to iTunes Connect. Products are: \(self.products)")    
            self.activitySpinnerStop()    
            self.showAlertWith(Localization("NoConnectionAlertTitle"), message: Localization("NoConnectionAlertMessage"))    
        }    
    }    
}    

// Restore purchases to this device.    
@IBAction func restoreTapped(_ sender: AnyObject) {    
    print("Restore button tapped...")    
    activitySpinnerStart()    
    IAPHelper.helper.restorePurchases()    
}    

@IBAction func playerEditorPressed(_ sender: Any) {    
    // First, make sure the user is authorised to make payments on his/her device    
    if IAPHelper.canMakePayments() {    
        // Then, make sure the product has been retrieved from ITC and placed in the products array. If not, show alert    
        if products.count > 0 {    
            activitySpinnerStart()    

            if let theProduct = self.products.first(where: {$0.productIdentifier == "com.xxx.xxx.PlayerEditor"}) {    
                IAPHelper.helper.buyProduct(theProduct)  // Purchasing the product. Fires productPurchased(notification:)    
            } else {    
                print("Could not find com.xxx.xxx.PlayerEditor in playerEditorPressed()")    
            }    

        } else {    
            showAlertWith(Localization("NoConnectionAlertTitle"), message: Localization("NoConnectionAlertMessage"))    
        }    

    } else {    
        showAlertWith(Localization("NoConnectionAlertTitle"), message: Localization("NoIAPAllowedMessage"))    
    }    
}    

@IBAction func removeAdsPressed(_ sender: Any) {    
    // First, make sure the user is authorised to make payments on his/her device    
    if IAPHelper.canMakePayments() {    
        // Then, make sure the product has been retrieved from ITC and placed in the products array. If not, show alert    
        if products.count > 0 {    
            activitySpinnerStart()    

            if let theProduct = self.products.first(where: {$0.productIdentifier == "com.xxx.xxx.RemoveAds"}) {    
                IAPHelper.helper.buyProduct(theProduct)  // Purchasing the product. Fires productPurchased(notification:)    
            } else {    
                print("Uh-oh, could not find com.xxx.xxx.RemoveAds in removeAdsPressed()")    
            }    

        } else {    
            showAlertWith(Localization("NoConnectionAlertTitle"), message: Localization("NoConnectionAlertMessage"))    
        }    

    } else {    
        showAlertWith(Localization("NoConnectionAlertTitle"), message: Localization("NoIAPAllowedMessage"))    
    }    
}    

// MARK: - App Store NSNotification methods    

// When a product is purchased, this notification fires    
@objc func productPurchased(_ notification: Notification) {    
    print("productPurchased(), message received in GameStoreVC")    
    let productIdentifier = notification.object as! String    
    for (index, product) in products.enumerated() {    
        if product.productIdentifier == productIdentifier {    
            activitySpinnerStop()    
            print("Successful purchase! index = \(index), product = \(product.localizedTitle)")    

            // Apply purchase to game    
            switch product.productIdentifier {    
            case "com.xxx.xxx.4000Coins":    
                var coinsTotal = retrieveNumberOfCoins()    
                coinsTotal += 4_000    
                UserDefaults.standard.set(coinsTotal, forKey: "Coins")    

            case "com.xxx.xxx.10000Coins":    
                var coinsTotal = retrieveNumberOfCoins()    
                coinsTotal += 10_000    
                UserDefaults.standard.set(coinsTotal, forKey: "Coins")    

            case "com.xxx.xxx.30000Coins":    
                var coinsTotal = retrieveNumberOfCoins()    
                coinsTotal += 30_000    
                UserDefaults.standard.set(coinsTotal, forKey: "Coins")    

            case "com.xxx.xxx.75000Coins":    
                var coinsTotal = retrieveNumberOfCoins()    
                coinsTotal += 75_000    
                UserDefaults.standard.set(coinsTotal, forKey: "Coins")    

            case "com.xxx.xxx.175000Coins":    
                var coinsTotal = retrieveNumberOfCoins()    
                coinsTotal += 175_000    
                UserDefaults.standard.set(coinsTotal, forKey: "Coins")    

            case "com.xxx.xxx.500000Coins":    
                var coinsTotal = retrieveNumberOfCoins()    
                coinsTotal += 500_000    
                UserDefaults.standard.set(coinsTotal, forKey: "Coins")    

            case "com.xxx.xxx.PlayerEditor":    
                print("Player Editor successfully bought!")    
                // 'Unlock' Player Editor by setting it to 1    
                UserDefaults.standard.set(1, forKey: "PlayerEditor")    

            case "com.xxx.xxx.RemoveAds":    
                print("Remove Ads successfully bought!")    
                // 'Unlock' Remove Ads by setting it to 1    
                UserDefaults.standard.set(1, forKey: "RemoveAds")    

            default:    
                print("No such productIdentifier found")    
            }    

        }    
    }    
}    

// When a transaction fails, this notification fires    
@objc func transactionFailed(_ notification: Notification) {    
    print("transactionFailed(), message received in GameStoreVC")    
    showAlertWith(Localization("TransactionFailedAlertTitle"), message: Localization("TransactionFailedAlertMessage"))    
}    

// When we cannot connect to iTunes to retrieve the IAPs, this notification fires    
@objc func cannotConnect(_ notification: Notification) {    
    print("cannotConnect(), message received in GameStoreVC")    
    showAlertWith(Localization("NoConnectionAlertTitle"), message: Localization("NoConnectionAlertMessage"))    
}    

}    

// MARK: - UITableViewDataSource    
extension GameStoreViewController: UITableViewDataSource {    

@objc func tableView(_ tableView: UITableView!, didSelectRowAtIndexPath indexPath: IndexPath!) {    
    // * COIN IAPs * //    
    if _selectionIndex == 1 {    
        // First, make sure the user is authorised to make payments on his/her device    
        if IAPHelper.canMakePayments() {    
            // Then, make sure the product has been retrieved and placed in the _coinProducts array. If not, show alert    
            if _coinProducts.count > 0 {    
                let product = _coinProducts[(indexPath as NSIndexPath).row]    
                IAPHelper.helper.buyProduct(product)  // Purchasing the product. Fires productPurchased(notification:)    

            } else {    
                showAlertWith(Localization("NoConnectionAlertTitle"), message: Localization("NoConnectionAlertMessage"))    
            }    

        } else {    
            showAlertWith(Localization("NoConnectionAlertTitle"), message: Localization("NoIAPAllowedMessage"))    
        }    

        _selectedCoinIndex = indexPath.row  // keep track of the selected coin index    
        iapTableView.reloadData()  // to change colour of selected cell and unselected cells    

    }    
}    

}    

更多信息:非消耗品(“Player Editor”和“Remove Ads”)工作正常,尽管它们的促销代码只能通过打开游戏商店并点击“Restore”来工作——它们在兑换后不能立即工作。

有人可以帮忙吗?!

标签: iosswiftin-app-purchasestorekit

解决方案


推荐阅读