Pembelian dalam aplikasi iOS: Memulai dan memproses pembelian

Halo semuanya, nama saya Vitaly, saya adalah pendiri Adapty. Kami melanjutkan rangkaian artikel yang dikhususkan untuk pembelian dalam aplikasi di aplikasi iOS. Di bagian sebelumnya, kami membahas proses pembuatan dan konfigurasi pembelian dalam aplikasi. Pada artikel ini, kami akan menganalisis pembuatan paywall sederhana (layar pembayaran), serta inisialisasi dan pemrosesan pembelian yang kami konfigurasikan di tahap pertama .



Buat layar langganan



Aplikasi apa pun yang menggunakan pembelian dalam aplikasi memiliki paywall. Ada persyaratan dari Apple yang menentukan set minimum elemen yang diperlukan dan teks penjelasan untuk layar tersebut. Pada tahap ini, kami tidak akan menjalankan semuanya seakurat mungkin, tetapi versi kami akan sangat mirip dengan versi yang berfungsi.



gambar


Jadi, layar kita akan terdiri dari elemen fungsional berikut:



  • Judul: penjelasan / blok penjualan.
  • Satu set tombol untuk memulai proses pembelian. Mereka juga akan menampilkan properti utama langganan: nama dan harga dalam mata uang lokal (mata uang toko).
  • Pulihkan tombol pembelian sebelumnya. Elemen ini diperlukan untuk semua aplikasi yang menggunakan langganan atau pembelian non-konsumsi.


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