我有一个将硬币作为消耗品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
}
}
}
更多信息:非消耗品(“玩家编辑器”和“删除广告”)可以正常工作,尽管它们的促销代码只能通过打开游戏商店并点击“恢复”来工作-赎回后它们不能直接工作。
有人可以帮忙吗?!