DYFStore 2.2.0

DYFStore 2.2.0

Tenfay维护。



DYFStore 2.2.0

  • Tenfay

DYFStore

DYFStore 是一个轻量级、易于使用的 iOS 库,用于 In-App Purchases。 (Swift)

DYFStore 使用代码块和 通知 来封装 StoreKit,提供 收据验证交易持久化

License MIT  CocoaPods  CocoaPods 

中文说明 (Chinese Instructions)

相关链接

功能特性

  • 非常简单的 In-App Purchases。
  • 内置支持记住您的购买。
  • 内置收据验证(远程)。
  • 内置托管内容下载和通知。

组 (ID:614799921)

安装

使用 CocoaPods

pod 'DYFStore'
or
pod 'DYFStore', '~> 2.0.2'

查看 wiki 了解更多选项。

用法

接下来我将向您展示如何使用 DYFStore

初始化

初始化如下。

  • 是否允许日志输出到控制台,在调试模式下设置为 'true',可以查看整个应用内购买的日志,发布应用时发布模式下设置为 'false'。
  • 添加事务观察者,监控事务的变化。
  • 实例化数据持久化对象,并存储事务的相关信息。
  • 遵循 DYFStoreAppStorePaymentDelegate 协议,处理从 App Store 购买的产品的支付。
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {

    self.initIAPSDK()

    return true
}

func initIAPSDK() {
    DYFStoreManager.shared.addStoreObserver()
    
    // Wether to allow the logs output to console.
    DYFStore.default.enableLog = true
    
    // Adds an observer that responds to updated transactions to the payment queue.
    // If an application quits when transactions are still being processed, those transactions are not lost. The next time the application launches, the payment queue will resume processing the transactions. Your application should always expect to be notified of completed transactions.
    // If more than one transaction observer is attached to the payment queue, no guarantees are made as to the order they will be called in. It is recommended that you use a single observer to process and finish the transaction.
    DYFStore.default.addPaymentTransactionObserver()
    
    // Sets the delegate processes the purchase which was initiated by user from the App Store.
    DYFStore.default.delegate = self
}

您可以从 App Store 用户触发的购买中处理购买,并使用 DYFStoreAppStorePaymentDelegate 协议提供您自己的实现。

// Processes the purchase which was initiated by user from the App Store.
func didReceiveAppStorePurchaseRequest(_ queue: SKPaymentQueue, payment: SKPayment, forProduct product: SKProduct) {
    if !DYFStore.canMakePayments() {
        self.showTipsMessage("Your device is not able or allowed to make payments!")
        return
    }
    
    // Get account name from your own user system.
    let accountName = "Handsome Jon"
    // This algorithm is negotiated with server developer.
    let userIdentifier = DYFStore_supplySHA256(accountName) ?? ""
    DYFStoreLog("userIdentifier: \(userIdentifier)")
    DYFStoreManager.shared.addPayment(product.productIdentifier, userIdentifier: userIdentifier)
}

请求产品

在请求产品之前,需要检查设备是否无法或无法进行支付。

if !DYFStore.canMakePayments() {
    self.showTipsMessage("Your device is not able or allowed to make payments!")
    return
}

在开始购变速序之前,您的应用必须知道其产品标识符。从 App Store 获取产品信息有两种策略。

策略 1:您的应用可以使用产品标识符来获取 App Store 中可供销售的产品信息,并直接提交支付请求。

@IBAction func fetchesProductAndSubmitsPayment(_ sender: Any) {
    // You need to check whether the device is not able or allowed to make payments before requesting product.
    if !DYFStore.canMakePayments() {
        self.showTipsMessage("Your device is not able or allowed to make payments!")
        return
    }
    self.showLoading("Loading...")
    
    let productId = "com.hncs.szj.coin42"
    DYFStore.default.requestProduct(withIdentifier: productId, success: { (products, invalidIdentifiers) in
        self.hideLoading()
        if products.count == 1 {
            let productId = products[0].productIdentifier
            self.addPayment(productId)
        } else {
            self.showTipsMessage("There is no this product for sale!")
        }
    }) { (error) in
        self.hideLoading()
        let value = error.userInfo[NSLocalizedDescriptionKey] as? String
        let msg = value ?? "\(error.localizedDescription)"
        self.sendNotice("An error occurs, \(error.code), " + msg)
    }
}

private func addPayment(_ productId: String) {
    // Get account name from your own user system.
    let accountName = "Handsome Jon"
    // This algorithm is negotiated with server developer.
    let userIdentifier = DYFStore_supplySHA256(accountName) ?? ""
    DYFStoreLog("userIdentifier: \(userIdentifier)")
    DYFStoreManager.shared.addPayment(productId, userIdentifier: userIdentifier)
}

策略2:它可以从应用商店检索产品信息并向用户展示其商店界面。您应用中每件销售的产品都有一个唯一的标识符。您的应用使用这些产品标识符获取应用商店中销售的产品信息,例如价格,并在用户购买这些产品时提交支付请求。

func fetchProductIdentifiersFromServer() -> [String] {
    let productIds = [
        "com.hncs.szj.coin42",   // 42 gold coins for ¥6.
        "com.hncs.szj.coin210",  // 210 gold coins for ¥30.
        "com.hncs.szj.coin686",  // 686 gold coins for ¥98.
        "com.hncs.szj.coin1386", // 1386 gold coins for ¥198.
        "com.hncs.szj.coin2086", // 2086 gold coins for ¥298.
        "com.hncs.szj.coin4886", // 4886 gold coins for ¥698.
        "com.hncs.szj.vip1",     // non-renewable vip subscription for a month.
        "com.hncs.szj.vip2"      // Auto-renewable vip subscription for three months.
    ]
    return productIds
}

@IBAction func fetchesProductsFromAppStore(_ sender: Any) {
    // You need to check whether the device is not able or allowed to make payments before requesting products.
    if !DYFStore.canMakePayments() {
        self.showTipsMessage("Your device is not able or allowed to make payments!")
        return
    }
    self.showLoading("Loading...")
    
    let productIds = fetchProductIdentifiersFromServer()
    DYFStore.default.requestProduct(withIdentifiers: productIds, success: { (products, invalidIdentifiers) in
        self.hideLoading()
        if products.count > 0 {
            self.processData(products)
        } else if products.count == 0 &&
                    invalidIdentifiers.count > 0 {
            // Please check the product information you set up.
            self.showTipsMessage("There are no products for sale!")
        }
    }) { (error) in
        self.hideLoading()
        let value = error.userInfo[NSLocalizedDescriptionKey] as? String
        let msg = value ?? "\(error.localizedDescription)"
        self.sendNotice("An error occurs, \(error.code), " + msg)
    }
}

private func processData(_ products: [SKProduct]) {
    var modelArray = [DYFStoreProduct]()
    for product in products {
        let p = DYFStoreProduct()
        p.identifier = product.productIdentifier
        p.name = product.localizedTitle
        p.price = product.price.stringValue
        p.localePrice = DYFStore.default.localizedPrice(ofProduct: product)
        p.localizedDescription = product.localizedDescription
        modelArray.append(p)
    }
    self.displayStoreUI(modelArray)
}

private func displayStoreUI(_ dataArray: [DYFStoreProduct]) {
    let storeVC = DYFStoreViewController()
    storeVC.dataArray = dataArray
    self.navigationController?.pushViewController(storeVC, animated: true)
}

添加支付

请求使用指定产品标识符的产品支付。

DYFStore.default.purchaseProduct("com.hncs.szj.coin210")

如果需要在您的系统中为用户账户添加支付并以不透明标识符的形式,您可以使用用户账户名称的单向哈希值来计算此属性的值。

计算SHA256哈希函数

public func DYFStore_supplySHA256(_ s: String) -> String? {
    guard let cStr = s.cString(using: String.Encoding.utf8) else {
        return nil
    }
    let digestLength = Int(CC_SHA256_DIGEST_LENGTH) // 32
    let cStrLen = Int(s.lengthOfBytes(using: String.Encoding.utf8))
    
    // Confirm that the length of C string is small enough
    // to be recast when calling the hash function.
    if cStrLen > UINT32_MAX {
        print("C string too long to hash: \(s)")
        return nil
    }
    
    let md = UnsafeMutablePointer<CUnsignedChar>.allocate(capacity: digestLength)
    CC_SHA256(cStr, CC_LONG(cStrLen), md)
    // Convert the array of bytes into a string showing its hex represention.
    let hash = NSMutableString()
    for i in 0..<digestLength {
        // Add a dash every four bytes, for readability.
        if i != 0 && i%4 == 0 {
            //hash.append("-")
        }
        hash.appendFormat("%02x", md[i])
    }
    md.deallocate()
    
    return hash as String
}

请求使用给定产品标识符、系统中的用户账户不透明标识符的产品支付。

DYFStore.default.purchaseProduct("com.hncs.szj.coin210", userIdentifier: "A43512564ACBEF687924646CAFEFBDCAEDF4155125657")

恢复交易

  • 不带有用户账户标识符恢复交易。
DYFStore.default.restoreTransactions()
  • 带有用户账户标识符恢复交易。
DYFStore.default.restoreTransactions(userIdentifier: "A43512564ACBEF687924646CAFEFBDCAEDF4155125657")

刷新收据

如果Bundle.main.appStoreReceiptURL为null,您需要创建一个刷新收据请求以获取支付交易收据。

DYFStore.default.refreshReceipt(onSuccess: {
    self.storeReceipt()
}) { (error) in
    self.failToRefreshReceipt()
}

通知

DYFStore发送与StoreKit相关事件的通知,并扩展了NSNotification以提供相关信息。为了接收它们,将观察者添加到DYFStore管理器中。

添加商店观察者

func addStoreObserver() {
    NotificationCenter.default.addObserver(self, selector: #selector(DYFStoreManager.processPurchaseNotification(_:)), name: DYFStore.purchasedNotification, object: nil)
    NotificationCenter.default.addObserver(self, selector: #selector(DYFStoreManager.processDownloadNotification(_:)), name: DYFStore.downloadedNotification, object: nil)
}

移除存储观察者

当应用程序退出时,您需要移除存储观察者。

func removeStoreObserver() {
    NotificationCenter.default.removeObserver(self, name: DYFStore.purchasedNotification, object: nil)
    NotificationCenter.default.removeObserver(self, name: DYFStore.downloadedNotification, object: nil)
}

支付交易通知

在请求支付或每个恢复的交易后,将发送支付交易通知。

@objc private func processPurchaseNotification(_ notification: Notification) {
    self.hideLoading()
    self.purchaseInfo = (notification.object as! DYFStore.NotificationInfo)
    switch self.purchaseInfo.state! {
    case .purchasing:
        self.showLoading("Purchasing...")
        break
    case .cancelled:
        self.sendNotice("You cancel the purchase")
        break
    case .failed:
        self.sendNotice(String(format: "An error occurred, \(self.purchaseInfo.error!.code)"))
        break
    case .succeeded, .restored:
        self.completePayment()
        break
    case .restoreFailed:
        self.sendNotice(String(format: "An error occurred, \(self.purchaseInfo.error!.code)"))
        break
    case .deferred:
        DYFStoreLog("Deferred")
        break
    }
}

下载通知

@objc private func processDownloadNotification(_ notification: Notification) {
    self.downloadInfo = (notification.object as! DYFStore.NotificationInfo)
    switch self.downloadInfo.downloadState! {
    case .started:
        DYFStoreLog("The download started")
        break
    case .inProgress:
        DYFStoreLog("The download progress: \(self.downloadInfo.downloadProgress)%%")
        break
    case .cancelled:
        DYFStoreLog("The download cancelled")
        break
    case .failed:
        DYFStoreLog("The download failed")
        break
    case .succeeded:
        DYFStoreLog("The download succeeded: 100%%")
        break
    }
}

收据验证

DYFStore 默认不执行收据验证,但提供参考实现。您可以实现自己的自定义验证或使用库提供的参考验证器。

以下概述了参考验证器。更多信息请参阅 维基百科

参考验证器

您通过懒加载创建并返回收据验证器(DYFStoreReceiptVerifier)。

lazy var receiptVerifier: DYFStoreReceiptVerifier = {
    let verifier = DYFStoreReceiptVerifier()
    verifier.delegate = self
    return verifier
}()

收据验证器委派收据验证,使您能够使用 DYFStoreReceiptVerifierDelegate 协议实现自己的实现。

public func verifyReceiptDidFinish(_ verifier: DYFStoreReceiptVerifier, didReceiveData data: [String : Any]) {}

public func verifyReceipt(_ verifier: DYFStoreReceiptVerifier, didFailWithError error: NSError) {}

您可以从 app store 服务器(从客户端上传的参数 -> S -> App Store S -> S -> 接收并解析数据 -> C,其中 C:客户端,S:服务器)验证应用内购买收据。

// Fetches the data of the bundle’s App Store receipt. 
let data = receiptData
or
let data = try? Data(contentsOf: DYFStore.receiptURL())

self.receiptVerifier.verifyReceipt(data)

// Only used for receipts that contain auto-renewable subscriptions.
//self.receiptVerifier.verifyReceipt(data, sharedSecret: "A43512564ACBEF687924646CAFEFBDCAEDF4155125657")

如果安全性是考虑因素,您可能想避免使用开源验证逻辑,而是提供自己的自定义验证器。

最好使用自己的服务器从客户端获取上传的参数,以验证来自应用商店服务器的收据。

完成交易

只有当客户端和服务器采用安全通信和数据加密,并且通过收据验证后,交易才能完成。这样,我们可以避免刷新订单和破解应用内购买。如果我们无法完成验证,我们希望StoreKit继续提醒我们还有未完成的交易。

DYFStore.default.finishTransaction(transaction)

交易持久化

DYFStore为存储交易在NSUserDefaults(DYFStoreUserDefaultsPersistence) 提供了一个可选的参考实现。

当客户端在支付过程中崩溃时,存储交易信息尤为重要。当StoreKit再次通知未完成的支付时,它直接从钥匙串中获取数据,并执行收据验证,直到交易完成。

存储交易

func storeReceipt() {
    guard let url = DYFStore.receiptURL() else {
        self.refreshReceipt()
        return
    }
    do {
        let data = try Data(contentsOf: url)
        let info = self.purchaseInfo!
        let persister =  DYFStoreUserDefaultsPersistence()
        
        let transaction = DYFStoreTransaction()
        if info.state! == .succeeded {
            transaction.state = DYFStoreTransactionState.purchased.rawValue
        } else if info.state! == .restored {
            transaction.state = DYFStoreTransactionState.restored.rawValue
        }
        
        transaction.productIdentifier = info.productIdentifier
        transaction.userIdentifier = info.userIdentifier
        transaction.transactionTimestamp = info.transactionDate?.timestamp()
        transaction.transactionIdentifier = info.transactionIdentifier
        transaction.originalTransactionTimestamp = info.originalTransactionDate?.timestamp()
        transaction.originalTransactionIdentifier = info.originalTransactionIdentifier
        
        transaction.transactionReceipt = data.base64EncodedString()
        persister.storeTransaction(transaction)
        
        self.verifyReceipt(data)
    } catch let error {
        DYFStoreLog("error: \(error.localizedDescription)")
        self.refreshReceipt()
        return
    }
}

删除交易

let info = self.purchaseInfo!
let store = DYFStore.default
let persister = DYFStoreUserDefaultsPersistence()
let identifier = info.transactionIdentifier!

if info.state! == .restored {
    let transaction = store.extractRestoredTransaction(identifier)
    store.finishTransaction(transaction)
} else {
    let transaction = store.extractPurchasedTransaction(identifier)
    // The transaction can be finished only after the receipt verification passed under the client and the server can adopt the communication of security and data encryption. In this way, we can avoid refreshing orders and cracking in-app purchase. If we were unable to complete the verification we want StoreKit to keep reminding us of the transaction.
    store.finishTransaction(transaction)
}

persister.removeTransaction(identifier)

if let id = info.originalTransactionIdentifier {
    persister.removeTransaction(id)
}

要求

DYFStore需支持iOS 8.0或以上版本以及ARC

演示代码

想要了解更多信息,请将此项目(git clone https://github.com/chenxing640/DYFStore.git)克隆到本地目录。

欢迎反馈

如果您发现任何问题,请创建一个议题。我将很高兴为您提供帮助。