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

版本记录

版本号 时间
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)
  }
}

后记

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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,425评论 4 361
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,058评论 1 291
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,186评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,848评论 0 204
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,249评论 3 286
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,554评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,830评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,536评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,239评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,505评论 2 244
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,004评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,346评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,999评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,060评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,821评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,574评论 2 271
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,480评论 2 267

推荐阅读更多精彩内容