Welcome to OGeek Q&A Community for programmer and developer-Open, Learning and Share
Welcome To Ask or Share your Answers For Others

Categories

0 votes
428 views
in Technique[技术] by (71.8m points)

ios - In-app purchase in application

I have several in-app purchases in application. I use this code:

@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)
    }

But I have a problem. When I click on my purchase button my code call this function - paymentQueueRestoreCompletedTransactionsFinished(_ queue: SKPaymentQueue) and if else check works. And my Userdefaults set true for key. As a result user unblock content but not pay for purchases. How to fix it?

See Question&Answers more detail:os

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
Welcome To Ask or Share your Answers For Others

1 Reply

0 votes
by (71.8m points)

It's a little hard to debug what you're doing as you're updating UserDefaults in two places and your purchase tracking code is tightly coupled to your purchasing code.

I would separate the concerns of purchasing and tracking purchases so you only have to keep track of and update or unlock them in one place. Something like this...

First off I'd separate all the iTunesConnect purchasing code into separate discreet classes (one for the iTunesStore, and one for the iTunesStore callback observer), create models to represent the purchase states and error states and create callbacks to notify the app of significant actions that happen during the product validation and purchase flow.

The callback protocols would look something like this:

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)
}

My iTunesStore class would look like this, and it would handle all interactions with iTunesConnect (or AppStoreConnect now):

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)
    }
}

You'll notice this class makes use of PurchaseStatus, PurchaseState, and PurchaseError objects to communicate status changes and updates to the app.

These classes look like this:

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."
        }
    }
}

With these classes in place we only have two more pieces to setup our store and make it easily reusable across apps without having to re-write large portions every time we want to add in app purchase in an app.

The next piece is the observer that receives callbacks from StoreKit, the iTunesStore class should be the only class that uses this:

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."))
    }
}

The last piece is a store class that manages triggering purchases and handles granting access to purchased products:

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.
    /

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
OGeek|极客中国-欢迎来到极客的世界,一个免费开放的程序员编程交流平台!开放,进步,分享!让技术改变生活,让极客改变未来! Welcome to OGeek Q&A Community for programmer and developer-Open, Learning and Share
Click Here to Ask a Question

...