ios - 应用程序内购买

标签 ios swift

我在应用程序中有几个应用程序内购买。我使用这段代码:

@IBAction func purchaseFull(_ sender: Any) {  

        purchase = "purchaseFull"

        product_id = "purchaseFull"

        print("About to fetch the product...")
        //self.loading.startAnimating()
        SKPaymentQueue.default().add(self)
        // Can make payments
        if (SKPaymentQueue.canMakePayments())
        {
            let productID:NSSet = NSSet(object: self.product_id!);
            let productsRequest:SKProductsRequest = SKProductsRequest(productIdentifiers: productID as! Set<String>);
            productsRequest.delegate = self;
            productsRequest.start();
            print("Fetching Products");
        }else{
            print("Can't make purchases");
        }
    }

@IBAction func purchase(_ sender: Any) {

        purchase = "purchase"

        product_id = "purchase\(index)"

        print("About to fetch the product...")
        //self.loading.startAnimating()
        SKPaymentQueue.default().add(self)
        // Can make payments
        if (SKPaymentQueue.canMakePayments())
        {
            let productID:NSSet = NSSet(object: self.product_id!);
            let productsRequest:SKProductsRequest = SKProductsRequest(productIdentifiers: productID as! Set<String>);
            productsRequest.delegate = self;
            productsRequest.start();
            print("Fetching Products");
        }else{
            print("Can't make purchases");
        }
    }

func productsRequest (_ request: SKProductsRequest, didReceive response: SKProductsResponse) {

        let count : Int = response.products.count
        if (count>0) {
            let validProduct: SKProduct = response.products[0] as SKProduct
            if (validProduct.productIdentifier == self.product_id) {
                print(validProduct.localizedTitle)
                print(validProduct.localizedDescription)
                print(validProduct.price)
                buyProduct(product: validProduct);

            } else {
                print(validProduct.productIdentifier)
            }
        } else {
            print("nothing")
        }
    }

    func buyProduct(product: SKProduct){
        print("Sending the Payment Request to Apple");
        let payment = SKPayment(product: product)
        SKPaymentQueue.default().add(payment);
        //self.loading.stopAnimating()
    }

    func request(_ request: SKRequest, didFailWithError error: Error) {
        print("Error Fetching product information");
        //self.loading.stopAnimating()
    }

    func paymentQueue(_ queue: SKPaymentQueue,
                      updatedTransactions transactions: [SKPaymentTransaction]) {
        print("Received Payment Transaction Response from Apple");

        for transaction:AnyObject in transactions {
            if let trans:SKPaymentTransaction = transaction as? SKPaymentTransaction{
                switch trans.transactionState {
                case .purchased:
                    print("Product Purchased");
                    SKPaymentQueue.default().finishTransaction(transaction as! SKPaymentTransaction)
                    // Handle the purchase

                    if purchase == "purchase" {
                        UserDefaults.standard.set(true , forKey: "purchase\(index)")
                    }

                    if purchase == "purchaseFull" {
                        UserDefaults.standard.set(true , forKey: "purchaseFull")
                    }

                    viewDidLoad()
                    break;
                case .failed:
                    print("Purchased Failed");
                    SKPaymentQueue.default().finishTransaction(transaction as! SKPaymentTransaction)
                    break;

                case .restored:
                    print("Already Purchased");
                    SKPaymentQueue.default().restoreCompletedTransactions()
                    // Handle the purchase
                    //UserDefaults.standard.set(true , forKey: "purchased")
                    viewDidLoad()
                    break;
                default:
                    break;
                }
            }
        }
    }

    @IBAction func restoreAction(_ sender: Any) {
        SKPaymentQueue.default().add(self)
        if (SKPaymentQueue.canMakePayments()) {
            SKPaymentQueue.default().restoreCompletedTransactions()
        }
    }

    func requestDidFinish(_ request: SKRequest) {

    }

    func paymentQueueRestoreCompletedTransactionsFinished(_ queue: SKPaymentQueue) {
        print("transactions restored")
        for transaction in queue.transactions {
            let t: SKPaymentTransaction = transaction
            let prodID = t.payment.productIdentifier as String

            if prodID == "purchaseFull" {
                print("action for restored")
                queue.finishTransaction(t)
                UserDefaults.standard.set(true , forKey: "purchaseFull")
            } else if prodID == "purchase0" {
                print("action0")
                queue.finishTransaction(t)
                UserDefaults.standard.set(true , forKey: "purchase0")
            } else if prodID == "purchase1" {
                print("action1")
                queue.finishTransaction(t)
                UserDefaults.standard.set(true , forKey: "purchase1")
            } else if prodID == "purchase2" {
                print("action2")
                queue.finishTransaction(t)
                UserDefaults.standard.set(true , forKey: "purchase2")
            } else if prodID == "purchase3" {
                print("action3")
                queue.finishTransaction(t)
                UserDefaults.standard.set(true , forKey: "purchase3")
            } else if prodID == "purchase4" {
                print("action4")
                queue.finishTransaction(t)
                UserDefaults.standard.set(true , forKey: "purchase4")
            } else if prodID == "purchase5" {
                print("action5")
                queue.finishTransaction(t)
                UserDefaults.standard.set(true , forKey: "purchase5")
            }
        }
        cancelAction((Any).self)
    }

但是我有一个问题。当我点击我的购买按钮时,我的代码调用这个函数 - paymentQueueRestoreCompletedTransactionsFinished(_ queue: SKPaymentQueue) 和 if else 检查工作。我的 Userdefaults 为 key 设置了 true。结果,用户取消阻止内容但不支付购买费用。如何解决?

最佳答案

调试您正在做的事情有点困难,因为您在两个地方更新 UserDefaults,并且您的购买跟踪代码与您的购买代码紧密耦合。

我会将购买和跟踪购买的关注点分开,因此您只需在一个地方跟踪并更新或解锁它们。像这样的……

首先,我将所有 iTunesConnect 购买代码分成单独的谨慎类(一个用于 iTunesStore,一个用于 iTunesStore 回调观察器),创建模型来表示购买状态和错误状态,并创建回调以通知应用程序在产品验证和购买流程中发生的重要操作。

回调协议(protocol)看起来像这样:

import StoreKit

/// Defines callbacks that will occur when products are being validated with the iTunes Store.
protocol iTunesProductStatusReceiver: class {
    func didValidateProducts(_ products: [SKProduct])
    func didReceiveInvalidProductIdentifiers(_ identifiers: [String])
}

/// Defines callbacks that occur during the purchase or restore process
protocol iTunesPurchaseStatusReceiver: class {
    func purchaseStatusDidUpdate(_ status: PurchaseStatus)
    func restoreStatusDidUpdate(_ status: PurchaseStatus)
}

我的 iTunesStore 类看起来像这样,它将处理与 iTunesConnect(或现在的 AppStoreConnect)的所有交互:

import Foundation
import StoreKit

class iTunesStore: NSObject, SKProductsRequestDelegate {

    weak var delegate: (iTunesProductStatusReceiver & iTunesPurchaseStatusReceiver)?

    var transactionObserver: IAPObserver = IAPObserver()
    var availableProducts: [SKProduct] = []
    var invalidProductIDs: [String] = []

    deinit {
        SKPaymentQueue.default().remove(self.transactionObserver)
    }

    override init() {
        super.init()
        transactionObserver.delegate = self
    }

    func fetchStoreProducts(identifiers:Set<String>) {
        print("Sending products request to ITC")
        let request:SKProductsRequest = SKProductsRequest.init(productIdentifiers: identifiers)
        request.delegate = self
        request.start()
    }

    func purchaseProduct(identifier:String) {
        guard let product = self.product(identifier: identifier) else {
            print("No products found with identifier: \(identifier)")

            // fire purchase status: failed notification
            delegate?.purchaseStatusDidUpdate(PurchaseStatus.init(state: .failed, error: PurchaseError.productNotFound, transaction: nil, message:"An error occured"))
            return
        }

        guard SKPaymentQueue.canMakePayments() else {
            print("Unable to make purchases...")
            delegate?.purchaseStatusDidUpdate(PurchaseStatus.init(state: .failed, error: PurchaseError.unableToPurchase, transaction: nil, message:"An error occured"))
            return
        }

        // Fire purchase began notification
        delegate?.purchaseStatusDidUpdate(PurchaseStatus.init(state: .initiated, error: nil, transaction: nil, message:"Processing Purchase"))

        let payment = SKPayment.init(product: product)
        SKPaymentQueue.default().add(payment)

    }

    func restorePurchases() {
        // Fire purchase began notification
        delegate?.restoreStatusDidUpdate(PurchaseStatus.init(state: .initiated, error: nil, transaction: nil, message:"Restoring Purchases"))
        SKPaymentQueue.default().restoreCompletedTransactions()
    }


    // returns a product for a given identifier if it exists in our available products array
    func product(identifier:String) -> SKProduct? {
        for product in availableProducts {
            if product.productIdentifier == identifier {
                return product
            }
        }

        return nil
    }

}

// Receives purchase status notifications and forwards them to this classes delegate
extension iTunesStore: iTunesPurchaseStatusReceiver {
    func purchaseStatusDidUpdate(_ status: PurchaseStatus) {
        delegate?.purchaseStatusDidUpdate(status)
    }

    func restoreStatusDidUpdate(_ status: PurchaseStatus) {
        delegate?.restoreStatusDidUpdate(status)
    }
}

// MARK: SKProductsRequest Delegate Methods
extension iTunesStore {
    @objc(productsRequest:didReceiveResponse:) func productsRequest(_ request: SKProductsRequest, didReceive response: SKProductsResponse) {
        // set new products
        availableProducts = response.products

        // set invalid product id's
        invalidProductIDs = response.invalidProductIdentifiers
        if invalidProductIDs.isEmpty == false {
            // call delegate if we received any invalid identifiers
            delegate?.didReceiveInvalidProductIdentifiers(invalidProductIDs)
        }
        print("iTunes Store: Invalid product IDs: \(response.invalidProductIdentifiers)")

        // call delegate with available products.
        delegate?.didValidateProducts(availableProducts)
    }
}

您会注意到此类使用 PurchaseStatus、PurchaseState 和 PurchaseError 对象来向应用传达状态更改和更新。

这些类看起来像这样:

import Foundation
import StoreKit

enum PurchaseState {
    case initiated
    case complete
    case cancelled
    case failed
}


class PurchaseStatus {
    var state:PurchaseState
    var error:Error?
    var transaction:SKPaymentTransaction?
    var message:String

    init(state:PurchaseState, error:Error?, transaction:SKPaymentTransaction?, message:String) {
        self.state = state
        self.error = error
        self.transaction = transaction
        self.message = message
    }
}

public enum PurchaseError: Error {
    case productNotFound
    case unableToPurchase

    public var code: Int {
        switch self {
        case .productNotFound:
            return 100101
        case .unableToPurchase:
            return 100101
        }
    }

    public var description: String {
        switch self {
        case .productNotFound:
            return "No products found for the requested product ID."
        case .unableToPurchase:
            return "Unable to make purchases. Check to make sure you are signed into a valid itunes account and that you are allowed to make purchases."
        }
    }

    public var title: String {
        switch self {
        case .productNotFound:
            return "Product Not Found"
        case .unableToPurchase:
            return "Unable to Purchase"
        }
    }

    public var domain: String {
        return "com.myAppId.purchaseError"
    }

    public var recoverySuggestion: String {
        switch self {
        case .productNotFound:
            return "Try again later."
        case .unableToPurchase:
            return "Check to make sure you are signed into a valid itunes account and that you are allowed to make purchases."
        }
    }
}

有了这些类,我们只需要另外两个部分来设置我们的商店,并使其可以轻松地跨应用重用,而无需在每次我们想在应用中添加应用购买时重新编写大部分内容。

下一 block 是从StoreKit接收回调的观察者,iTunesStore类应该是唯一使用它的类:

import Foundation
import StoreKit


class IAPObserver: NSObject, SKPaymentTransactionObserver {

    // delegate to propagate status update up
    weak var delegate: iTunesPurchaseStatusReceiver?

    override init() {
        super.init()
        SKPaymentQueue.default().add(self)
    }

    func paymentQueue(_ queue: SKPaymentQueue, updatedTransactions transactions: [SKPaymentTransaction]) {
        for transaction in transactions {
            switch transaction.transactionState {
                case .purchasing:   // Transaction is being added to the server queue
                    break

                case .purchased:    // Transaction is in queue, user has been charged. Complete transaction now
                    // Notify purchase complete status
                    delegate?.purchaseStatusDidUpdate(PurchaseStatus.init(state: .complete, error: nil, transaction: transaction, message:"Purchase Complete."))
                    SKPaymentQueue.default().finishTransaction(transaction)

                case .failed:   // Transaction was cancelled or failed before being added to the server queue
                        // An error occured, notify
                    delegate?.purchaseStatusDidUpdate(PurchaseStatus.init(state: .failed, error: transaction.error, transaction: transaction, message:"An error occured."))
                    SKPaymentQueue.default().finishTransaction(transaction)

                case .restored: // transaction was rewtored from the users purchase history. Complete transaction now.
                    // notify purchase completed with status... success
                    delegate?.restoreStatusDidUpdate(PurchaseStatus.init(state: .complete, error: nil, transaction: transaction, message:"Restore Success!"))
                    SKPaymentQueue.default().finishTransaction(transaction)

                case .deferred: // transaction is in the queue, but it's final status is pending user/external action
                    break
            }
        }
    }

    func paymentQueueRestoreCompletedTransactionsFinished(_ queue: SKPaymentQueue) {
        guard queue.transactions.count > 0 else {
            // Queue does not include any transactions, so either user has not yet made a purchase
            // or the user's prior purchase is unavailable, so notify app (and user) accordingly.

            print("restore queue.transaction.count === 0")
            return
        }

        for transaction in queue.transactions {
            // TODO: provide content access here??
            print("Product restored with id: \(String(describing: transaction.original?.payment.productIdentifier))")
            SKPaymentQueue.default().finishTransaction(transaction)
        }
    }

    func paymentQueue(_ queue: SKPaymentQueue, restoreCompletedTransactionsFailedWithError error: Error) {
        // fire notification to dismiss spinner, restore error
        delegate?.restoreStatusDidUpdate(PurchaseStatus.init(state: .failed, error: error, transaction: nil, message:"Restore Failed."))
    }
}

最后一部分是商店类,它管理触发购买并处理授予对已购买产品的访问权限:

enum ProductIdentifier: String {
    case one = "com.myprefix.id1"
    case two = "com.myprefix.id2"

    static func from(rawValue: String) -> ProductIdentifier? {
        switch rawValue {
        case one.rawValue: return .one
        case two.rawValue: return .two
        default: return nil
        }
    }
}

class Store {
    static let shared = Store()

    // purchase processor
    var paymentProcessor: iTunesStore = iTunesStore()

    init() {
        // register for purchase status update callbacks
        paymentProcessor.delegate = self

        validateProducts()
    }

    // validates products with the iTunesConnect store for faster purchase processing
    // when a user wants to buy
    internal func validateProducts() {

        // all products to validate
        let products = [
            ProductIdentifier.one.rawValue,
            ProductIdentifier.two.rawValue
        ]

        paymentProcessor.fetchStoreProducts(identifiers: Set.init(products))
    }

    /// Purchase a product by specifying the product identifier.
    ///
    /// - Parameter identifier: The product identifier for the product being purchased. This must belong to a valid product in the 'products' array, as this array is searched for a product with the specified identifier. If none are found this function bails.
    func purchaseProduct(identifier: ProductIdentifier) {
        print("purchase product: \(identifier)")

        self.paymentProcessor.purchaseProduct(identifier: identifier.rawValue)
    }

    /// Initiates restore purchases functionality.
    func restorePurchases() {
        self.paymentProcessor.restorePurchases()
    }

    /// This function is called during the purchase/restore purchase process with each status change in the flow. If the status is complete then access to the product should be granted at this point.
    ///
    /// - Parameter status: The current status of the transaction.
    internal func processPurchaseStatus(_ status: PurchaseStatus) {
        switch status.state {
        case .initiated:
            // TODO: show alert that purchase is in progress...
            break
        case .complete:
            if let productID = status.transaction?.payment.productIdentifier {
                // Store product id in UserDefaults or some other method of tracking purchases
                UserDefaults.standard.set(true , forKey: productID)
                UserDefaults.standard.synchronize()
            }
        case .cancelled:
            break
        case .failed:
            // TODO: notify user with alert...
            break
        }
    }
}

extension Store: iTunesPurchaseStatusReceiver, iTunesProductStatusReceiver {
    func purchaseStatusDidUpdate(_ status: PurchaseStatus) {
        // process based on received status
        processPurchaseStatus(status)
    }

    func restoreStatusDidUpdate(_ status: PurchaseStatus) {
        // pass this into the same flow as purchasing for unlocking products
        processPurchaseStatus(status)
    }

    func didValidateProducts(_ products: [SKProduct]) {
        print("Product identifier validation complete with products: \(products)")
        // TODO: if you have a local representation of your products you could
        // sync them up with the itc version here
    }

    func didReceiveInvalidProductIdentifiers(_ identifiers: [String]) {
        // TODO: filter out invalid products? maybe... by default isActive is false
    }
}

通过使用这种方法,您将能够将所有与 iTunesConnect 相关的类放在一个文件夹中并跨项目使用它们,并且只需为您要在其中使用它的每个项目更新您的 ProductIdentifer 枚举和 Store 类。

祝你好运!希望这对您有所帮助!

编辑:这是一个示例应用程序 (Swift 4),其中集成了所有内容,展示了如何使用它 - https://github.com/appteur/eziap

编辑 2:(回答有关显示警报的评论)

您可以通过多种方式显示通知用户的警报。

您可以通过发送 Notification 并让 View Controller 监听它来在任何 View Controller 中触发警报。您可以设置一个委托(delegate)链到适当的 View Controller ,您还可以创建将传递给 Store 对象并在状态更改时更新的闭包。

我可能会创建一个可选的自定义警报 View Controller ,并在其类型的 Store 类中设置一个变量,并将其呈现在最顶层的 View Controller 上。

我会在启动状态的internal func processPurchaseStatus(_ status: PurchaseStatus) 函数中显示它,并在同一函数中状态发生变化时更新它。

我会用当前状态更新警报 View 上的标签,并让它在成功购买时自动关闭或显示成功屏幕。如果交易失败,我会用错误更新警报消息并显示一个按钮以关闭警报 View 。

我使用这样的扩展来获取最顶层的 View Controller :

import UIKit

extension UIApplication {
    static func topViewController(controller: UIViewController? = UIApplication.shared.keyWindow?.rootViewController) -> UIViewController? {
        if let navigationController = controller as? UINavigationController {
            return topViewController(controller: navigationController.visibleViewController)
        }
        if let tabController = controller as? UITabBarController {
            if let selected = tabController.selectedViewController {
                return topViewController(controller: selected)
            }
        }
        if let presented = controller?.presentedViewController {
            return topViewController(controller: presented)
        }
        return controller
    }
}

使用这种方法你可能会有这样的功能:

internal func showAlert() {

    // do UI stuff on the main thread
    DispatchQueue.main.async { [weak self] in

        // load alert from storyboard, nib, or some method, get the topmost view controller in the controller hierarchy, only use it if it's not being dismissed
        guard let alertVC = MyAlertClass.fromNib(), let topVC = UIApplication.topViewController(), topVC.isBeingDismissed == false else {
            return
        }

        // set our local variable so we can update the message later and dismiss it more easily
        self.myAlertController = alertVC

        // Present the alert view controller
        topVC.present(alertVC, animated: true, completion: nil)
    }
}

除了自定义警报 View Controller 之外,您还可以使用警报 UI 创建一个 View 子类,并将其添加到最顶层 View Controller 的 View 中。

关于ios - 应用程序内购买,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/51679047/

相关文章:

ios - 如何安全地删除我的 ~/Library/Developer/Xcode/DerivedData 目录?

ios - 从 CloudKit 获取不在设备上的记录的任何(或最佳)方法?

ios - Sprite Kit中的Cocos2d ccpForAngle()函数替换是什么

android - Firebase:报告分析和崩溃数据以分离项目

ios - PHImageManager requestImageForAsset 有时会为 iCloud 照片返回 nil

ios - 带休息套件的 POST

ios - 使用SK3DNode在SpriteKit中显示SceneKit粒子对象

ios - 带有 switch 语句的 SwiftUI 动画

ios - UITableViewCell 中的 UICollectionView?

ios - 为什么我要在 swift 中创建一个单例而不是仅仅将函数转储到一个文件中?