Compras en la aplicación de iOS: inicializar y procesar compras

Hola a todos, mi nombre es Vitaly, soy el fundador de Adapty. Continuamos con nuestra serie de artículos dedicados a las compras integradas en aplicaciones iOS. En la parte anterior, cubrimos el proceso de creación y configuración de compras dentro de la aplicación. En este artículo analizaremos la creación de un paywall simple (pantalla de pago), así como la inicialización y procesamiento de las compras que configuramos en la primera etapa .



Crear una pantalla de suscripción



Cualquier aplicación que utilice compras dentro de la aplicación tiene un muro de pago. Hay requisitos de Apple que definen el conjunto mínimo de elementos requeridos y textos explicativos para tales pantallas. En esta etapa, no los realizaremos todos con la mayor precisión posible, pero nuestra versión estará muy cerca de la versión de trabajo.



imagen


Entonces, nuestra pantalla constará de los siguientes elementos funcionales:



  • Título: bloques explicativos / de venta.
  • Un conjunto de botones para iniciar el proceso de compra. También mostrarán las principales propiedades de las suscripciones: nombre y precio en moneda local (la moneda de la tienda).
  • Botón Restaurar compras pasadas. Este elemento es necesario para todas las aplicaciones que utilizan suscripciones o compras no consumibles.


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