美文网首页
iOS内购IAP(十五) —— IAP的收据验证(二)

iOS内购IAP(十五) —— IAP的收据验证(二)

作者: 刀客传奇 | 来源:发表于2019-01-09 10:39 被阅读87次

版本记录

版本号 时间
V1.0 2019.01.09

前言

大家都知道,iOS虚拟商品如宝石、金币等都需要走内购,和苹果三七分成,如果这类商品不走内购那么上不去架或者上架以后被发现而被下架。最近有一个项目需要增加内购支付功能,所以最近又重新集成并整理了下,希望对大家有所帮助。感兴趣的可以参考上面几篇。
1. iOS内购IAP(一) —— 基础配置篇(一)
2. iOS内购IAP(二) —— 工程实践(一)
3. iOS内购IAP(三) —— 编程指南之关于内购(一)
4. iOS内购IAP(四) —— 编程指南之设计您的应用程序的产品(一)
5. iOS内购IAP(五) —— 编程指南之检索产品信息(一)
6. iOS内购IAP(六) —— 编程指南之请求支付(一)
7. iOS内购IAP(七) —— 编程指南之促进应用内购买(一)
8. iOS内购IAP(八) —— 编程指南之提供产品(一)
9. iOS内购IAP(九) —— 编程指南之处理订阅(一)
10. iOS内购IAP(十) —— 编程指南之恢复购买的产品(一)
11. iOS内购IAP(十一) —— 编程指南之准备App审核(一)
12. iOS内购IAP(十二) —— 一个详细的内购流程(一)
13. iOS内购IAP(十三) —— 一个详细的内购流程(二)
14. iOS内购IAP(十四) —— IAP的收据验证(一)

源码

1. Swift

首先看下工程组织结构

接着看下sb中的内容

下面就是源码部分了

1. ASN1Helpers.swift
import UIKit

func readASN1Data(ptr: UnsafePointer<UInt8>, length: Int) -> Data {
  return Data(bytes: ptr, count: length)
}

func readASN1Integer(ptr: inout UnsafePointer<UInt8>?, maxLength: Int) -> Int? {
  var type: Int32 = 0
  var xclass: Int32 = 0
  var length: Int = 0
  
  ASN1_get_object(&ptr, &length, &type, &xclass, maxLength)
  guard type == V_ASN1_INTEGER else {
    return nil
  }
  let integerObject = c2i_ASN1_INTEGER(nil, &ptr, length)
  let intValue = ASN1_INTEGER_get(integerObject)
  ASN1_INTEGER_free(integerObject)
  
  return intValue
}

func readASN1String(ptr: inout UnsafePointer<UInt8>?, maxLength: Int) -> String? {
  var strClass: Int32 = 0
  var strLength = 0
  var strType: Int32 = 0
  
  var strPointer = ptr
  ASN1_get_object(&strPointer, &strLength, &strType, &strClass, maxLength)
  if strType == V_ASN1_UTF8STRING {
    let p = UnsafeMutableRawPointer(mutating: strPointer!)
    let utfString = String(bytesNoCopy: p, length: strLength, encoding: .utf8, freeWhenDone: false)
    return utfString
  }
  
  if strType == V_ASN1_IA5STRING {
    let p = UnsafeMutablePointer(mutating: strPointer!)
    let ia5String = String(bytesNoCopy: p, length: strLength, encoding: .ascii, freeWhenDone: false)
    return ia5String
  }
  
  return nil
}

func readASN1Date(ptr: inout UnsafePointer<UInt8>?, maxLength: Int) -> Date? {
  var str_xclass: Int32 = 0
  var str_length = 0
  var str_type: Int32 = 0
  
  // A date formatter to handle RFC 3339 dates in the GMT time zone
  let formatter = DateFormatter()
  formatter.locale = Locale(identifier: "en_US_POSIX")
  formatter.dateFormat = "yyyy'-'MM'-'dd'T'HH':'mm':'ss'Z'"
  formatter.timeZone = TimeZone(abbreviation: "GMT")
  
  var strPointer = ptr
  ASN1_get_object(&strPointer, &str_length, &str_type, &str_xclass, maxLength)
  guard str_type == V_ASN1_IA5STRING else {
    return nil
  }

  let p = UnsafeMutableRawPointer(mutating: strPointer!)
  if let dateString = String(bytesNoCopy: p, length: str_length, encoding: .ascii, freeWhenDone: false) {
    return formatter.date(from: dateString)
  }

  return nil
}
2. IAPReceipt.swift
import Foundation

struct IAPReceipt {
  var quantity: Int?
  var productIdentifier: String?
  var transactionIdentifer: String?
  var originalTransactionIdentifier: String?
  var purchaseDate: Date?
  var originalPurchaseDate: Date?
  var subscriptionExpirationDate: Date?
  var subscriptionIntroductoryPricePeriod: Int?
  var subscriptionCancellationDate: Date?
  var webOrderLineId: Int?
  
  init?(with pointer: inout UnsafePointer<UInt8>?, payloadLength: Int) {
    let endPointer = pointer!.advanced(by: payloadLength)
    var type: Int32 = 0
    var xclass: Int32 = 0
    var length = 0
    
    ASN1_get_object(&pointer, &length, &type, &xclass, payloadLength)
    guard type == V_ASN1_SET else {
      return nil
    }
    
    while pointer! < endPointer {
      ASN1_get_object(&pointer, &length, &type, &xclass, pointer!.distance(to: endPointer))
      guard type == V_ASN1_SEQUENCE else {
        return nil
      }
      guard let attributeType = readASN1Integer(ptr: &pointer,
                                                maxLength: pointer!.distance(to: endPointer))
        else {
          return nil
      }
      // Attribute version must be an integer, but not using the value
      guard let _ = readASN1Integer(ptr: &pointer,
                                    maxLength: pointer!.distance(to: endPointer))
        else {
          return nil
      }
      ASN1_get_object(&pointer, &length, &type, &xclass, pointer!.distance(to: endPointer))
      guard type == V_ASN1_OCTET_STRING else {
        return nil
      }
      
      switch attributeType {
      case 1701:
        var p = pointer
        quantity = readASN1Integer(ptr: &p, maxLength: length)
      case 1702:
        var p = pointer
        productIdentifier = readASN1String(ptr: &p, maxLength: length)
      case 1703:
        var p = pointer
        transactionIdentifer = readASN1String(ptr: &p, maxLength: length)
      case 1705:
        var p = pointer
        originalTransactionIdentifier = readASN1String(ptr: &p, maxLength: length)
      case 1704:
        var p = pointer
        purchaseDate = readASN1Date(ptr: &p, maxLength: length)
      case 1706:
        var p = pointer
        originalPurchaseDate = readASN1Date(ptr: &p, maxLength: length)
      case 1708:
        var p = pointer
        subscriptionExpirationDate = readASN1Date(ptr: &p, maxLength: length)
      case 1712:
        var p = pointer
        subscriptionCancellationDate = readASN1Date(ptr: &p, maxLength: length)
      case 1711:
        var p = pointer
        webOrderLineId = readASN1Integer(ptr: &p, maxLength: length)
      default:
        break
      }
      
      pointer = pointer!.advanced(by: length)
    }
  }
}
3. Receipt.swift
import UIKit

enum ReceiptStatus: String {
  case validationSuccess = "This receipt is valid."
  case noReceiptPresent = "A receipt was not found on this device."
  case unknownFailure = "An unexpected failure occurred during verification."
  case unknownReceiptFormat = "The receipt is not in PKCS7 format."
  case invalidPKCS7Signature = "Invalid PKCS7 Signature."
  case invalidPKCS7Type = "Invalid PKCS7 Type."
  case invalidAppleRootCertificate = "Public Apple root certificate not found."
  case failedAppleSignature = "Receipt not signed by Apple."
  case unexpectedASN1Type = "Unexpected ASN1 Type."
  case missingComponent = "Expected component was not found."
  case invalidBundleIdentifier = "Receipt bundle identifier does not match application bundle identifier."
  case invalidVersionIdentifier = "Receipt version identifier does not match application version."
  case invalidHash = "Receipt failed hash check."
  case invalidExpired = "Receipt has expired."
}

class Receipt {
  var receiptStatus: ReceiptStatus?
  var bundleIdString: String?
  var bundleVersionString: String?
  var bundleIdData: Data?
  var hashData: Data?
  var opaqueData: Data?
  var expirationDate: Date?
  var receiptCreationDate: Date?
  var originalAppVersion: String?
  var inAppReceipts: [IAPReceipt] = []

  static public func isReceiptPresent() -> Bool {
    if let receiptUrl = Bundle.main.appStoreReceiptURL,
      let canReach = try? receiptUrl.checkResourceIsReachable(),
      canReach {
      return true
    }
    
    return false
  }
  
  init() {
    guard let payload = loadReceipt() else {
      return
    }
    
    guard validateSigning(payload) else {
      return
    }
    
    readReceipt(payload)
    
    validateReceipt()
  }
  
  private func loadReceipt() -> UnsafeMutablePointer<PKCS7>? {
    // Load the receipt into a Data object
    guard
      let receiptUrl = Bundle.main.appStoreReceiptURL,
      let receiptData = try? Data(contentsOf: receiptUrl)
      else {
        receiptStatus = .noReceiptPresent
        return nil
    }
    
    // 1
    let receiptBIO = BIO_new(BIO_s_mem())
    let receiptBytes: [UInt8] = .init(receiptData)
    BIO_write(receiptBIO, receiptBytes, Int32(receiptData.count))
    // 2
    let receiptPKCS7 = d2i_PKCS7_bio(receiptBIO, nil)
    BIO_free(receiptBIO)
    // 3
    guard receiptPKCS7 != nil else {
      receiptStatus = .unknownReceiptFormat
      return nil
    }
    
    // Check that the container has a signature
    guard OBJ_obj2nid(receiptPKCS7!.pointee.type) == NID_pkcs7_signed else {
      receiptStatus = .invalidPKCS7Signature
      return nil
    }
    
    // Check that the container contains data
    let receiptContents = receiptPKCS7!.pointee.d.sign.pointee.contents
    guard OBJ_obj2nid(receiptContents?.pointee.type) == NID_pkcs7_data else {
      receiptStatus = .invalidPKCS7Type
      return nil
    }
    
    return receiptPKCS7
  }
  
  private func validateSigning(_ receipt: UnsafeMutablePointer<PKCS7>?) -> Bool {
    guard
      let rootCertUrl = Bundle.main
        .url(forResource: "AppleIncRootCertificate", withExtension: "cer"),
      let rootCertData = try? Data(contentsOf: rootCertUrl)
      else {
        receiptStatus = .invalidAppleRootCertificate
        return false
    }
    
    let rootCertBio = BIO_new(BIO_s_mem())
    let rootCertBytes: [UInt8] = .init(rootCertData)
    BIO_write(rootCertBio, rootCertBytes, Int32(rootCertData.count))
    let rootCertX509 = d2i_X509_bio(rootCertBio, nil)
    BIO_free(rootCertBio)
    
    // 1
    let store = X509_STORE_new()
    X509_STORE_add_cert(store, rootCertX509)
    
    // 2
    OPENSSL_init_crypto(UInt64(OPENSSL_INIT_ADD_ALL_DIGESTS), nil)
    
    // 3
    let verificationResult = PKCS7_verify(receipt, nil, store, nil, nil, 0)
    guard verificationResult == 1  else {
      receiptStatus = .failedAppleSignature
      return false
    }
    
    return true
  }
  
  private func readReceipt(_ receiptPKCS7: UnsafeMutablePointer<PKCS7>?) {
    // Get a pointer to the start and end of the ASN.1 payload
    let receiptSign = receiptPKCS7?.pointee.d.sign
    let octets = receiptSign?.pointee.contents.pointee.d.data
    var ptr = UnsafePointer(octets?.pointee.data)
    let end = ptr!.advanced(by: Int(octets!.pointee.length))
    
    var type: Int32 = 0
    var xclass: Int32 = 0
    var length: Int = 0
    
    ASN1_get_object(&ptr, &length, &type, &xclass, ptr!.distance(to: end))
    guard type == V_ASN1_SET else {
      receiptStatus = .unexpectedASN1Type
      return
    }

    // 1
    while ptr! < end {
      // 2
      ASN1_get_object(&ptr, &length, &type, &xclass, ptr!.distance(to: end))
      guard type == V_ASN1_SEQUENCE else {
        receiptStatus = .unexpectedASN1Type
        return
      }
      
      // 3
      guard let attributeType = readASN1Integer(ptr: &ptr, maxLength: length) else {
        receiptStatus = .unexpectedASN1Type
        return
      }
      
      // 4
      guard let _ = readASN1Integer(ptr: &ptr, maxLength: ptr!.distance(to: end)) else {
        receiptStatus = .unexpectedASN1Type
        return
      }
      
      // 5
      ASN1_get_object(&ptr, &length, &type, &xclass, ptr!.distance(to: end))
      guard type == V_ASN1_OCTET_STRING else {
        receiptStatus = .unexpectedASN1Type
        return
      }
      
      switch attributeType {
      case 2: // The bundle identifier
        var stringStartPtr = ptr
        bundleIdString = readASN1String(ptr: &stringStartPtr, maxLength: length)
        bundleIdData = readASN1Data(ptr: ptr!, length: length)
        
      case 3: // Bundle version
        var stringStartPtr = ptr
        bundleVersionString = readASN1String(ptr: &stringStartPtr, maxLength: length)
        
      case 4: // Opaque value
        let dataStartPtr = ptr!
        opaqueData = readASN1Data(ptr: dataStartPtr, length: length)
        
      case 5: // Computed GUID (SHA-1 Hash)
        let dataStartPtr = ptr!
        hashData = readASN1Data(ptr: dataStartPtr, length: length)
        
      case 12: // Receipt Creation Date
        var dateStartPtr = ptr
        receiptCreationDate = readASN1Date(ptr: &dateStartPtr, maxLength: length)
        
      case 17: // IAP Receipt
        var iapStartPtr = ptr
        let parsedReceipt = IAPReceipt(with: &iapStartPtr, payloadLength: length)
        if let newReceipt = parsedReceipt {
          inAppReceipts.append(newReceipt)
        }
        
      case 19: // Original App Version
        var stringStartPtr = ptr
        originalAppVersion = readASN1String(ptr: &stringStartPtr, maxLength: length)
        
      case 21: // Expiration Date
        var dateStartPtr = ptr
        expirationDate = readASN1Date(ptr: &dateStartPtr, maxLength: length)
        
      default: // Ignore other attributes in receipt
        print("Not processing attribute type: \(attributeType)")
      }
      
      // Advance pointer to the next item
      ptr = ptr!.advanced(by: length)      
    }
  }
  
  private func validateReceipt() {
    guard
      let idString = bundleIdString,
      let version = bundleVersionString,
      let _ = opaqueData,
      let hash = hashData
      else {
        receiptStatus = .missingComponent
        return
    }
    
    // Check the bundle identifier
    guard let appBundleId = Bundle.main.bundleIdentifier else {
      receiptStatus = .unknownFailure
      return
    }
    
    guard idString == appBundleId else {
      receiptStatus = .invalidBundleIdentifier
      return
    }
    
    // Check the version
    guard let appVersionString =
      Bundle.main.object(forInfoDictionaryKey: "CFBundleVersion") as? String else {
      receiptStatus = .unknownFailure
      return
    }
    guard version == appVersionString else {
      receiptStatus = .invalidVersionIdentifier
      return
    }
    
    // Check the GUID hash
    let guidHash = computeHash()
    guard hash == guidHash else {
      receiptStatus = .invalidHash
      return
    }
    
    // Check the expiration attribute if it's present
    let currentDate = Date()
    if let expirationDate = expirationDate {
      if expirationDate < currentDate {
        receiptStatus = .invalidExpired
        return
      }
    }
    
    // All checks passed so validation is a success
    receiptStatus = .validationSuccess
  }
  
  private func getDeviceIdentifier() -> Data {
    let device = UIDevice.current
    var uuid = device.identifierForVendor!.uuid
    let addr = withUnsafePointer(to: &uuid) { (p) -> UnsafeRawPointer in
      UnsafeRawPointer(p)
    }
    let data = Data(bytes: addr, count: 16)
    return data
  }
  
  private func computeHash() -> Data {
    let identifierData = getDeviceIdentifier()
    var ctx = SHA_CTX()
    SHA1_Init(&ctx)
    
    let identifierBytes: [UInt8] = .init(identifierData)
    SHA1_Update(&ctx, identifierBytes, identifierData.count)
    
    let opaqueBytes: [UInt8] = .init(opaqueData!)
    SHA1_Update(&ctx, opaqueBytes, opaqueData!.count)
    
    let bundleBytes: [UInt8] = .init(bundleIdData!)
    SHA1_Update(&ctx, bundleBytes, bundleIdData!.count)
    
    var hash: [UInt8] = .init(repeating: 0, count: 20)
    SHA1_Final(&hash, &ctx)
    return Data(bytes: hash, count: 20)
  }
}
4. ViewController.swift
import UIKit
import StoreKit

class ViewController: UIViewController {
  @IBOutlet weak var bundleIdentifier: UILabel!
  @IBOutlet weak var bundleVersion: UILabel!
  @IBOutlet weak var expirationDate: UILabel!
  @IBOutlet weak var verificationStatus: UILabel!
  @IBOutlet weak var buyButton: UIButton!
  @IBOutlet weak var iapTableView: UITableView!
  @IBOutlet weak var receiptCreationDate: UILabel!
  @IBOutlet weak var originalAppVersion: UILabel!
  
  // Receipt
  var receipt: Receipt?
  
  // Store
  public static let storeItem1 = "com.billmorefield.receiptverification.consumable"
  public static let storeItem2 = "com.billmorefield.receiptverification.nonconsumable"
  public static let storeItem3 = "com.billmorefield.receiptverification.nonconsumable2"
  private static let productIdentifiers: Set<ProductIdentifier> = [ViewController.storeItem1, ViewController.storeItem2, ViewController.storeItem3]
  public static let store = IAPHelper(productIds: ViewController.productIdentifiers)
  var products: [SKProduct] = []
  private lazy var dateFormatter: DateFormatter = {
    let formatter = DateFormatter()
    formatter.timeStyle = .short
    formatter.dateStyle = .medium
    return formatter
  }()
  
  override func viewDidLoad() {
    super.viewDidLoad()
    
    // Set table delegate
    iapTableView.dataSource = self

    // Set up store if payments allowed
    if IAPHelper.canMakePayments() {
      NotificationCenter.default.addObserver(self,
                                             selector: #selector(purchaseMade(notification:)),
                                             name: Notification.Name("IAPHelperPurchaseNotification"),
                                             object: nil)

      ViewController.store.requestProducts { (success, products) in
        if success {
          self.products = products!
          DispatchQueue.main.async {
            self.buyButton.isEnabled = true
          }
        }
      }
    }
    
    // If a receipt is present validate it, otherwise request to refresh it
    if Receipt.isReceiptPresent() {
      validateReceipt()
    } else {
      refreshReceipt()
    }
  }
  
  func refreshReceipt() {
    verificationStatus.text = "Requesting refresh of receipt."
    verificationStatus.textColor = .green
    print("Requesting refresh of receipt.")
    let refreshRequest = SKReceiptRefreshRequest()
    refreshRequest.delegate = self
    refreshRequest.start()
  }
  
  func formatDateForUI(_ date: Date) -> String {
    let formatter = DateFormatter()
    formatter.dateStyle = .medium
    formatter.timeStyle = .none
    return formatter.string(from: date)
  }
  
  func validateReceipt() {
    verificationStatus.text = "Validating Receipt..."
    verificationStatus.textColor = .green
    
    receipt = Receipt()
    if let receiptStatus = receipt?.receiptStatus {
      verificationStatus.text = receiptStatus.rawValue
      guard receiptStatus == .validationSuccess else {
        // If verification didn't succeed, then show status in red and clear other fields
        verificationStatus.textColor = .red
        bundleIdentifier.text = ""
        bundleVersion.text = ""
        expirationDate.text = ""
        originalAppVersion.text = ""
        receiptCreationDate.text = ""
        return
      }
      
      // If verification succeed, we show information contained in the receipt
      verificationStatus.textColor = .green
      bundleIdentifier.text = "Bundle Identifier: \(receipt!.bundleIdString!)"
      bundleVersion.text = "Bundle Version: \(receipt!.bundleVersionString!)"
      
      if let originalVersion = receipt?.originalAppVersion {
        originalAppVersion.text = "Original Version: \(originalVersion)"
      } else {
        originalAppVersion.text = "Not Provided"
      }
      
      if let receiptExpirationDate = receipt?.expirationDate {
        expirationDate.text = "Expiration Date: \(formatDateForUI(receiptExpirationDate))"
      } else {
        expirationDate.text = "Not Provided."
      }
      
      if let receiptCreation = receipt?.receiptCreationDate {
        receiptCreationDate.text = "Receipt Creation Date: \(formatDateForUI(receiptCreation))"
      } else {
        receiptCreationDate.text = "Not Provided."
      }
      
      iapTableView.reloadData()
    }
  }
  
  // MARK: - Buttons
  @IBAction func buyButtonTouched(_ sender: Any) {
    let alert = UIAlertController(title: "Select Purchcase",
                                  message: "Choose the item you wish to purchase",
                                  preferredStyle: .actionSheet)

    for product in products {
      alert.addAction(UIAlertAction(title: product.localizedTitle, style: .default) { _ in
        ViewController.store.buyProduct(product)
      })
    }

    alert.addAction(UIAlertAction(title: "Restore Purchases", style: .default) { _ in
      ViewController.store.restorePurchases()
    })

    alert.addAction(UIAlertAction(title: "Cancel", style: .cancel))
    
    present(alert, animated: true)
  }
  
  @IBAction func restoreButtonTouched(_ sender: Any) {
    ViewController.store.restorePurchases()
  }
  
  @IBAction func refreshReceiptTouched(_ sender: Any) {
    refreshReceipt()
  }

  // MARK: - Notification Handler
  @objc func purchaseMade(notification: NSNotification) {
  }
}

// MARK: - SKRequestDelegate extension
extension ViewController: SKRequestDelegate {
  func requestDidFinish(_ request: SKRequest) {
    if Receipt.isReceiptPresent() {
      print("Verifying newly refreshed receipt.")
      validateReceipt()
    }
  }
  
  func request(_ request: SKRequest, didFailWithError error: Error) {
    verificationStatus.text = error.localizedDescription
    print("StoreKit request failed: \(error.localizedDescription)")
    verificationStatus.textColor = .red
  }
}

// MARK: - UITableViewDataSource extension
extension ViewController: UITableViewDataSource {
  func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
    return "In App Purchases in Receipt"
  }
  
  func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
    if let iapItems = receipt?.inAppReceipts {
      return iapItems.count
    }
    
    return 0
  }
  
  func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
    let cell = tableView.dequeueReusableCell(withIdentifier: "IAPCell", for: indexPath) as! IAPTableViewCell
    guard let iapItem = receipt?.inAppReceipts[indexPath.row] else {
      cell.productIdentifier.text = "Unknown"
      cell.purchaseDate.text = ""
      return cell
    }
    
    cell.productIdentifier.text = iapItem.productIdentifier ?? "Unknown"
    if let date = iapItem.purchaseDate {
      cell.purchaseDate.text = dateFormatter.string(from: date)
    } else {
      cell.purchaseDate.text = ""
    }
    return cell
  }
}
5. IAPTableViewCell.swift
import UIKit

class IAPTableViewCell: UITableViewCell {
  @IBOutlet weak var productIdentifier: UILabel!
  @IBOutlet weak var purchaseDate: UILabel!
}
6. IAPHelper.swift
import StoreKit

public typealias ProductIdentifier = String
public typealias ProductsRequestCompletionHandler = (_ success: Bool, _ products: [SKProduct]?) -> Void

extension Notification.Name {
  static let IAPHelperPurchaseNotification = Notification.Name("IAPHelperPurchaseNotification")
}

open class IAPHelper: NSObject  {
  private let productIdentifiers: Set<ProductIdentifier>
  private var purchasedProductIdentifiers: Set<ProductIdentifier> = []
  private var productsRequest: SKProductsRequest?
  private var productsRequestCompletionHandler: ProductsRequestCompletionHandler?
  
  public init(productIds: Set<ProductIdentifier>) {
    productIdentifiers = productIds
    for productIdentifier in productIds {
      if UserDefaults.standard.bool(forKey: productIdentifier) {
        purchasedProductIdentifiers.insert(productIdentifier)
        print("Previously purchased: \(productIdentifier)")
      } else {
        print("Not purchased: \(productIdentifier)")
      }
    }
    super.init()

    SKPaymentQueue.default().add(self)
  }
}

// MARK: - StoreKit API

extension IAPHelper {
  public func requestProducts(_ completionHandler: @escaping ProductsRequestCompletionHandler) {
    productsRequest?.cancel()
    productsRequestCompletionHandler = completionHandler

    productsRequest = SKProductsRequest(productIdentifiers: productIdentifiers)
    productsRequest!.delegate = self
    productsRequest!.start()
  }

  public func buyProduct(_ product: SKProduct) {
    print("Buying \(product.productIdentifier)...")
    let payment = SKPayment(product: product)
    SKPaymentQueue.default().add(payment)
  }

  public func isProductPurchased(_ productIdentifier: ProductIdentifier) -> Bool {
    return purchasedProductIdentifiers.contains(productIdentifier)
  }
  
  public class func canMakePayments() -> Bool {
    return SKPaymentQueue.canMakePayments()
  }
  
  public func restorePurchases() {
    SKPaymentQueue.default().restoreCompletedTransactions()
  }
}

// MARK: - SKProductsRequestDelegate

extension IAPHelper: SKProductsRequestDelegate {
  public func productsRequest(_ request: SKProductsRequest, didReceive response: SKProductsResponse) {
    print("Loaded list of products...")
    let products = response.products
    productsRequestCompletionHandler?(true, products)
    clearRequestAndHandler()

    for product in products {
      print("Found product: \(product.productIdentifier) \(product.localizedTitle) \(product.price.floatValue)")
    }
  }

  public func request(_ request: SKRequest, didFailWithError error: Error) {
    print("Failed to load list of products.")
    print("Error: \(error.localizedDescription)")
    productsRequestCompletionHandler?(false, nil)
    clearRequestAndHandler()
  }

  private func clearRequestAndHandler() {
    productsRequest = nil
    productsRequestCompletionHandler = nil
  }
}

// MARK: - SKPaymentTransactionObserver

extension IAPHelper: SKPaymentTransactionObserver {
  public func paymentQueue(_ queue: SKPaymentQueue, updatedTransactions transactions: [SKPaymentTransaction]) {
    for transaction in transactions {
      switch (transaction.transactionState) {
      case .purchased:
        complete(transaction: transaction)
        break
      case .failed:
        fail(transaction: transaction)
        break
      case .restored:
        restore(transaction: transaction)
        break
      case .deferred:
        break
      case .purchasing:
        break
      }
    }
  }

  private func complete(transaction: SKPaymentTransaction) {
    print("complete...")
    deliverPurchaseNotificationFor(identifier: transaction.payment.productIdentifier)
    SKPaymentQueue.default().finishTransaction(transaction)
  }

  private func restore(transaction: SKPaymentTransaction) {
    guard let productIdentifier = transaction.original?.payment.productIdentifier else { return }

    print("restore... \(productIdentifier)")
    deliverPurchaseNotificationFor(identifier: productIdentifier)
    SKPaymentQueue.default().finishTransaction(transaction)
  }

  private func fail(transaction: SKPaymentTransaction) {
    print("fail...")
    if let transactionError = transaction.error as NSError?,
      let localizedDescription = transaction.error?.localizedDescription,
        transactionError.code != SKError.paymentCancelled.rawValue {
        print("Transaction Error: \(localizedDescription)")
      }

    SKPaymentQueue.default().finishTransaction(transaction)
  }

  private func deliverPurchaseNotificationFor(identifier: String?) {
    guard let identifier = identifier else { return }

    purchasedProductIdentifiers.insert(identifier)
    UserDefaults.standard.set(true, forKey: identifier)
    NotificationCenter.default.post(name: .IAPHelperPurchaseNotification, object: identifier)
  }
}

后记

本篇主要讲述了收据验证,感兴趣的给个赞或者关注~~~

相关文章

网友评论

      本文标题:iOS内购IAP(十五) —— IAP的收据验证(二)

      本文链接:https://www.haomeiwen.com/subject/aajwrqtx.html