美文网首页
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