Compras no aplicativo iOS: inicializar e processar compras

Olá a todos, meu nome é Vitaly, sou o fundador do Adapty. Continuamos a série de artigos dedicados a compras no aplicativo em aplicativos iOS. Na parte anterior, cobrimos o processo de criação e configuração de compras no aplicativo. Neste artigo, vamos analisar a criação do paywall mais simples (tela de pagamento), bem como a inicialização e processamento das compras que configuramos na primeira etapa .



Crie uma tela de inscrição



Qualquer aplicativo que usa compras no aplicativo tem um acesso pago. Existem requisitos da Apple que definem o conjunto mínimo de elementos necessários e textos explicativos para tais telas. Nesta fase, não realizaremos todos eles com a maior precisão possível, mas nossa versão estará muito próxima da versão de trabalho.



imagem


Portanto, nossa tela consistirá nos seguintes elementos funcionais:



  • Título: blocos explicativos / de venda.
  • Um conjunto de botões para iniciar o processo de compra. Eles também mostrarão as principais propriedades das assinaturas: nome e preço na moeda local (a moeda da loja).
  • Botão Restaurar compras anteriores. Este elemento é necessário para todos os aplicativos que usam assinaturas ou compras de itens não consumíveis.


Interface Builder Storyboard. ViewController, UI (UIActivityIndicatorView) , .





ViewController. , .



import StoreKit
import UIKit

class ViewController: UIViewController {

    // 1:
    @IBOutlet private weak var purchaseButtonA: UIButton!
    @IBOutlet private weak var purchaseButtonB: UIButton!
    @IBOutlet private weak var activityIndicator: UIActivityIndicatorView!

    override func viewDidLoad() {
        super.viewDidLoad()
        activityIndicator.hidesWhenStopped = true

        // 2:
        showSpinner()
        Purchases.default.initialize { [weak self] result in
            guard let self = self else { return }
            self.hideSpinner()

            switch result {
            case let .success(products):
                DispatchQueue.main.async {
                    self.updateInterface(products: products)
                }
            default:
                break
            }
        }
    }

    // 3:
    private func updateInterface(products: [SKProduct]) {
        updateButton(purchaseButtonA, with: products[0])
        updateButton(purchaseButtonB, with: products[1])
    }

    // 4:
    @IBAction func purchaseAPressed(_ sender: UIButton) { }

    @IBAction func purchaseBPressed(_ sender: UIButton) { }

        @IBAction func restorePressed(_ sender: UIButton) { }
}


  1. - UI
  2. viewDidLoad . , , UI, . , — . -, .
  3. , , , .
  4. - .


:



extension ViewController {
    // 1:
    func updateButton(_ button: UIButton, with product: SKProduct) {
        let title = "\(product.title ?? product.productIdentifier) for \(product.localizedPrice)"
        button.setTitle(title, for: .normal)
    }

    func showSpinner() {
        DispatchQueue.main.async {
            self.activityIndicator.startAnimating()
            self.activityIndicator.isHidden = false
        }
    }

    func hideSpinner() {
        DispatchQueue.main.async {
            self.activityIndicator.stopAnimating()
        }
    }
}Spinner


, (1) SKProduct. , extension :



extension SKProduct {
    var localizedPrice: String {
        let formatter = NumberFormatter()
        formatter.numberStyle = .currency
        formatter.locale = priceLocale
        return formatter.string(from: price)!
    }

    var title: String? {
        switch productIdentifier {
        case "barcode_month_subscription":
            return "Monthly Subscription"
        case "barcode_year_subscription":
            return "Annual Subscription"
        default:
            return nil
        }
    }
}


Purchases



. Apple. Purchases , , SKProduct .



typealias RequestProductsResult = Result<[SKProduct], Error>
typealias PurchaseProductResult = Result<Bool, Error>

typealias RequestProductsCompletion = (RequestProductsResult) -> Void
typealias PurchaseProductCompletion = (PurchaseProductResult) -> Void

class Purchases: NSObject {
    static let `default` = Purchases()

    private let productIdentifiers = Set<String>(
        arrayLiteral: "barcode_month_subscription", "barcode_year_subscription"
    )

    private var products: [String: SKProduct]?
    private var productRequest: SKProductsRequest?

    func initialize(completion: @escaping RequestProductsCompletion) {
        requestProducts(completion: completion)
    }

    private var productsRequestCallbacks = [RequestProductsCompletion]()

    private func requestProducts(completion: @escaping RequestProductsCompletion) {
        guard productsRequestCallbacks.isEmpty else {
            productsRequestCallbacks.append(completion)
            return
        }

        productsRequestCallbacks.append(completion)

        let productRequest = SKProductsRequest(productIdentifiers: productIdentifiers)
        productRequest.delegate = self
        productRequest.start()

        self.productRequest = productRequest
    }
}


Delegate:



extension Purchases: SKProductsRequestDelegate {
        guard !response.products.isEmpty else {
            print("Found 0 products")

            productsRequestCallbacks.forEach { $0(.success(response.products)) }
            productsRequestCallbacks.removeAll()
            return
        }

        var products = [String: SKProduct]()
        for skProduct in response.products {
            print("Found product: \(skProduct.productIdentifier)")
            products[skProduct.productIdentifier] = skProduct
        }

        self.products = products

        productsRequestCallbacks.forEach { $0(.success(response.products)) }
        productsRequestCallbacks.removeAll()
    }

    func request(_ request: SKRequest, didFailWithError error: Error) {
        print("Failed to load products with error:\n \(error)")

        productsRequestCallbacks.forEach { $0(.failure(error)) }
        productsRequestCallbacks.removeAll()
    }
}




, , , enum PurchaseError, Error ( LocalizedError):



enum PurchasesError: Error {
    case purchaseInProgress
    case productNotFound
    case unknown
}


StoreKit, ( ).



purchaseProduct , restorePurchases — ( non-consumable ):



        fileprivate var productPurchaseCallback: ((PurchaseProductResult) -> Void)?

    func purchaseProduct(productId: String, completion: @escaping (PurchaseProductResult) -> Void) {
        // 1:
        guard productPurchaseCallback == nil else {
            completion(.failure(PurchasesError.purchaseInProgress))
            return
        }
        // 2:
        guard let product = products?[productId] else {
            completion(.failure(PurchasesError.productNotFound))
            return
        }

        productPurchaseCallback = completion

        // 3:
        let payment = SKPayment(product: product)
        SKPaymentQueue.default().add(payment)
    }

    public func restorePurchases(completion: @escaping (PurchaseProductResult) -> Void) {
        guard productPurchaseCallback == nil else {
            completion(.failure(PurchasesError.purchaseInProgress))
            return
        }
        productPurchaseCallback = completion
        // 4:
        SKPaymentQueue.default().restoreCompletedTransactions()
    }


  1. , ( , , , , )
  2. peoductId,
  3. SKPaymentQueue
  4. , SKPaymentQueue


, , SKPaymentTransactionObserver:



extension Purchases: SKPaymentTransactionObserver {
    func paymentQueue(_ queue: SKPaymentQueue, updatedTransactions transactions: [SKPaymentTransaction]) {
        // 1:
        for transaction in transactions {
            switch transaction.transactionState {
            // 2:
            case .purchased, .restored:
                if finishTransaction(transaction) {
                    SKPaymentQueue.default().finishTransaction(transaction)
                    productPurchaseCallback?(.success(true))
                } else {
                    productPurchaseCallback?(.failure(PurchasesError.unknown))
                }
            // 3:
            case .failed:
                productPurchaseCallback?(.failure(transaction.error ?? PurchasesError.unknown))
                SKPaymentQueue.default().finishTransaction(transaction)
            default:
                break
            }
        }

                productPurchaseCallback = nil
    }
}

extension Purchases {
    // 4:
    func finishTransaction(_ transaction: SKPaymentTransaction) -> Bool {
        let productId = transaction.payment.productIdentifier
        print("Product \(productId) successfully purchased")
        return true
    }
}


  1. ,
  2. , purchased restored, , , /, , finishTransaction. : consumable , , , .
  3. , .
  4. , 2: (, , UI , )


. purchasing (, ) deferred — (, ). UI.





ViewController, , , .



        @IBAction func purchaseAPressed(_ sender: UIButton) {
        showSpinner()
        Purchases.default.purchaseProduct(productId: "barcode_month_subscription") { [weak self] _ in
            self?.hideSpinner()
            // Handle result
        }
    }

    @IBAction func purchaseBPressed(_ sender: Any) {
        showSpinner()
        Purchases.default.purchaseProduct(productId: "barcode_year_subscription") { [weak self] _ in
            self?.hideSpinner()
            // Handle result
        }
    }

    @IBAction func restorePressed(_ sender: UIButton) {
        showSpinner()
        Purchases.default.restorePurchases { [weak self] _ in
            self?.hideSpinner()
            // Handle result
        }
    }


, . . x401om .




All Articles