Xin chào mọi người. Phần 1 mình đã hướng dẫn các bạn setup môi trường và setup payments rồi. Bài này mình sẽ tiếp tục với implement code và thực hiện test sau khi implement.
Implement
Đầu tiên chúng ta enable In-App Purchase trong Capability
Bắt đầu implememt code
Ở bài trước mình đã tạo một item tên là Apple Book và đặt productID là Demo.TechoverInAppPurchase.co.Apple.Book
Cũng trong bài trước mình có nhấn mạnh productID rất quan trọng và phải matching trong code, bên dưới đây mình sẽ thực hiện điều đó.
Đầu tiên mình tạo ra một file plist mình để tên là IAPProductIDs, mục đích của file này là để quản lý tất cả các purchase producIDs của ứng dụng, thường thì một ứng dụng sử dụng In-App Purchase sẽ có nhiều item thanh toán, cũng như nhiều giá trị khác nhau, mỗi giá item đó tương ứng với một productID, Hiện tại demo của mình chỉ có 1 item thanh toán vì thế trong file plist của mình chỉ có một item và value của item đó là productID.
Tiếp theo mình tạo ra một class IAPManager như bên dưới:
import Foundation
import StoreKit
class IAPManager: NSObject {
// MARK: - Custom Types
enum IAPManagerError: Error {
case noProductIDsFound
case noProductsFound
case paymentWasCancelled
case productRequestFailed
}
// MARK: - Properties
static let shared = IAPManager()
private var onReceiveProductsHandler: ((Result<[SKProduct], IAPManagerError>) -> Void)?
private var onBuyProductHandler: ((Result<Bool, Error>) -> Void)?
var totalRestoredPurchases = 0
// MARK: - Init
private override init() {
super.init()
}
// MARK: - General Methods
fileprivate func getProductIDs() -> [String]? {
guard let url = Bundle.main.url(forResource: "IAPProductIDs", withExtension: "plist") else { return nil }
do {
let data = try Data(contentsOf: url)
let productIDs = try PropertyListSerialization.propertyList(from: data, options: .mutableContainersAndLeaves, format: nil) as? [String] ?? []
return productIDs
} catch {
print(error.localizedDescription)
return nil
}
}
func getPriceFormatted(for product: SKProduct) -> String? {
let formatter = NumberFormatter()
formatter.numberStyle = .currency
formatter.locale = product.priceLocale
return formatter.string(from: product.price)
}
func startObserving() {
SKPaymentQueue.default().add(self)
}
func stopObserving() {
SKPaymentQueue.default().remove(self)
}
func canMakePayments() -> Bool {
return SKPaymentQueue.canMakePayments()
}
// MARK: - Get IAP Products
func getProducts(withHandler productsReceiveHandler: @escaping (_ result: Result<[SKProduct], IAPManagerError>) -> Void) {
onReceiveProductsHandler = productsReceiveHandler
guard let productIDs = getProductIDs() else {
productsReceiveHandler(.failure(.noProductIDsFound))
return
}
let request = SKProductsRequest(productIdentifiers: Set(productIDs))
request.delegate = self
request.start()
}
// MARK: - Purchase Products
func buy(product: SKProduct, withHandler handler: @escaping ((_ result: Result<Bool, Error>) -> Void)) {
let payment = SKPayment(product: product)
SKPaymentQueue.default().add(payment)
onBuyProductHandler = handler
}
func restorePurchases(withHandler handler: @escaping ((_ result: Result<Bool, Error>) -> Void)) {
onBuyProductHandler = handler
totalRestoredPurchases = 0
SKPaymentQueue.default().restoreCompletedTransactions()
}
}
// MARK: - SKPaymentTransactionObserver
extension IAPManager: SKPaymentTransactionObserver {
func paymentQueue(_ queue: SKPaymentQueue, updatedTransactions transactions: [SKPaymentTransaction]) {
transactions.forEach { (transaction) in
switch transaction.transactionState {
case .purchased:
onBuyProductHandler?(.success(true))
SKPaymentQueue.default().finishTransaction(transaction)
case .restored:
totalRestoredPurchases += 1
SKPaymentQueue.default().finishTransaction(transaction)
case .failed:
if let error = transaction.error as? SKError {
if error.code != .paymentCancelled {
onBuyProductHandler?(.failure(error))
} else {
onBuyProductHandler?(.failure(IAPManagerError.paymentWasCancelled))
}
print("IAP Error:", error.localizedDescription)
}
SKPaymentQueue.default().finishTransaction(transaction)
case .deferred, .purchasing: break
@unknown default: break
}
}
}
func paymentQueueRestoreCompletedTransactionsFinished(_ queue: SKPaymentQueue) {
if totalRestoredPurchases != 0 {
onBuyProductHandler?(.success(true))
} else {
print("IAP: No purchases to restore!")
onBuyProductHandler?(.success(false))
}
}
func paymentQueue(_ queue: SKPaymentQueue, restoreCompletedTransactionsFailedWithError error: Error) {
if let error = error as? SKError {
if error.code == .paymentCancelled {
onBuyProductHandler?(.failure(IAPManagerError.paymentWasCancelled))
} else {
print("IAP Restore Error:", error.localizedDescription)
onBuyProductHandler?(.failure(error))
}
}
}
}
// MARK: - SKProductsRequestDelegate
extension IAPManager: SKProductsRequestDelegate {
func productsRequest(_ request: SKProductsRequest, didReceive response: SKProductsResponse) {
let products = response.products
if !products.isEmpty {
onReceiveProductsHandler?(.success(products))
} else {
onReceiveProductsHandler?(.failure(.noProductsFound))
}
}
func request(_ request: SKRequest, didFailWithError error: Error) {
onReceiveProductsHandler?(.failure(.productRequestFailed))
}
func requestDidFinish(_ request: SKRequest) {
// Handle finish
}
}
// MARK: - IAPManagerError Localized Error Descriptions
extension IAPManager.IAPManagerError: LocalizedError {
var errorDescription: String? {
switch self {
case .noProductIDsFound:
return "No In-App Purchase product identifiers were found."
case .noProductsFound:
return "No In-App Purchases were found."
case .productRequestFailed:
return "Unable to fetch available In-App Purchase products at the moment."
case .paymentWasCancelled:
return "In-App Purchase process was cancelled."
}
}
}
Ở class trên mình đã viết đầy đủ các methods như: buy(), restorePurchases()… Cũng như xử lý các error.
Bây giờ chúng ta sử dụng IAPManager để thực hiện purchase nhé.
Đầu tiên mình thực hiện layout một để một button có tên là buy ở giữa màn hình:
Tiếp theo mình thực hiện implement việc purchase trong class ViewController
import UIKit
import StoreKit
class ViewController: UIViewController {
private var products: [SKProduct] = []
override func viewDidLoad() {
super.viewDidLoad()
}
override func viewWillAppear(_ animated: Bool) {
IAPManager.shared.getProducts { (result) in
DispatchQueue.main.async {
switch result {
case .success(let products):
self.products = products
case .failure(let error):
self.showAlert(message: error.localizedDescription)
}
}
}
}
// MARK: - Actions
@IBAction func didBuy(_ sender: UIButton) {
DispatchQueue.main.async {
self.verifyBeforeBuy()
}
}
// MARK: - Private Methods
private func verifyBeforeBuy() {
guard let product = product(with: "Demo.TechoverInAppPurchase.co.Apple.Book"),
let price = IAPManager.shared.getPriceFormatted(for: product) else {
return
}
let alertController = UIAlertController(title: product.localizedTitle,
message: product.localizedDescription,
preferredStyle: .alert)
alertController.addAction(UIAlertAction(title: "Buy now for \(price)", style: .default, handler: { (_) in
self.buy(product: product)
}))
alertController.addAction(UIAlertAction(title: "Cancel", style: .cancel, handler: nil))
self.present(alertController, animated: true, completion: nil)
}
private func product(with id: String) -> SKProduct? {
return products.first(where: { $0.productIdentifier == id })
}
private func buy(product: SKProduct) {
if !IAPManager.shared.canMakePayments() {
self.showAlert(message: "In-App Purchases are not allowed in this device.")
} else {
IAPManager.shared.buy(product: product) { result in
switch result {
case .success(let success):
if success {
self.showAlert(message: "Buy success")
} else {
self.showAlert(message: "Buy error")
}
case .failure(let failure):
self.showAlert(message: failure.localizedDescription)
}
}
}
}
}
extension ViewController {
func showAlert(title:String = "",
message: String) {
let alert = UIAlertController(title: title, message: message, preferredStyle: .alert)
alert.addAction(UIAlertAction(title: "OK", style: .default, handler: nil))
self.present(alert, animated: true, completion: nil)
}
}
Mình sẽ giải thích flow của việc purchase như sau:
- Ở viewWillAppear() mình lấy tất cả producIDs đã set trong IAPProductIDs.plist và lưu lại với biến products: [SKProduct]
- Khi người dùng bấm vào buy button mình thực hiện verify trước khi gọi đến IAPManager.shared.buy(product: product)
- verifyBeforeBuy(): mình lấy product có id là Demo.TechoverInAppPurchase.co.Apple.Book, từ product đó mình lấy ra price của product đó bằng method getPriceFormatted trong IAPManager và thông báo cho người dùng số tiền cần bỏ ra để mua item này
- Khi người dùng bấm confirm mua trên alert mình thực hiện kiểm tra xem device có cho phép thực hiện giao dịch hay không bằng method IAPManager.shared.canMakePayments, nếu có thì gọi đến IAPManager.shared.buy và nếu không cho phép thì hiển thị thông báo cho người dùng
- IAPManager.shared.buy(): sau khi thực hiện method này hệ thống sẽ show một actionsheet để người dùng xác nhận trước khi thực hiện giao dịch
- Sau khi thực hiện giao dịch nếu thành công sẽ nhận được một thông báo và thất bại thì mình cũng sẽ hiển thị thông báo cho người dùng
Test In-App Purchase sau khi implement
Trên thực tế khi thực hiện mỗi giao dịch đều mất tiền, vậy mỗi khi test in-app purchase thì đều mất tiền sao? Câu trả lời là không, vì apple đã support viêc testing thông qua apple sandbox account, điều này giúp developer cũng như tester thực hiện giao dịch mà không mất tiền.
Chúng ta vào đây để add sandbox account. Mình add account [email protected]
Tiếp theo chúng ta thực hiện login sandbox account trên device test.
Setting -> App Store -> Sandbox account -> login
Sau khi add sandbox account và login, chúng ta thực hiện purchase thì sẽ nhận được thông báo là đang purchase với account sandbox và không mất phí:
Thông qua phần 1 và bài này mình đã giới thiệu đến các bạn cách để thực hiện in-app purchase trong Swift.
Mình hi vọng bài viết có thể giúp ích được cho các bạn. Chúc các bạn thành công!
1 comment
cảm ơn rất nhiều! quá tuyệt vời!