版本记录
版本号 | 时间 |
---|---|
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)
}
}
后记
本篇主要讲述了收据验证,感兴趣的给个赞或者关注~~~
网友评论