SwiftyReceiptValidator 7.0.0

SwiftyReceiptValidator 7.0.0

Dominik Ringler 维护。



  • Dominik Ringler

Swift 5.0 Platform SPM supported CocoaPods Compatible

SwiftyReceiptValidator

一个用于处理 App Store 交易验证的 Swift 库。

在你上线之前

  • 测试,测试,再测试

请进行适当的测试,包括生产模式,使用苹果的生产服务器 URL。使用 Xcode 的发布模式进行测试以确保一切正常工作。这不是一件可以轻率对待的事情,当您的应用程序处于发布模式时,要对购买功能进行三次检查。

要求

  • iOS 12.0+
  • Swift 5.0+

安装

Swift 包管理器

Swift包管理器是一个自动分发Swift代码的工具,它集成到了Swift编译器中。

要将Swift包添加到项目中,只需在xCode中打开项目,然后点击“文件”>“Swift包”>“添加包依赖”。然后输入https://github.com/crashoverride777/swifty-receipt-validator.git作为仓库URL,并完成安装向导。

或者,如果您有另一个需要将SwiftyReceiptValidator作为依赖项的Swift包,将其添加到Package.swift的依赖项值中同样简单。

dependencies: [
.package(url: "https://github.com/crashoverride777/swifty-receipt-validator.git", from: "6.1.0")
]

Cocoa Pods

Cocoa Pods是一个针对Cocoa项目的依赖管理器。只需在pod文件中添加以下行来安装pods:

pod 'SwiftyReceiptValidator'

手动操作

或者,您可以将“源”文件夹及其包含的文件拖动到项目中。

使用

添加导入(如果使用CocoaPods或SwiftPackageManager)

  • 当您通过SwiftPackageManager或CocoaPods安装时,将导入语句添加到您的swift文件中。
import SwiftyReceiptValidator

初始化收据验证器

在处理内购的类中实例化SwiftyReceiptValidator

  • 自定义配置(推荐)

苹果公司官方建议进行收据验证时,应连接到您自己的服务器,然后您的服务器再连接到苹果的服务器以验证收据。

class SomeClass {
    let receiptValidator: SwiftyReceiptValidatorType
    
    init() {
        let configuration = SRVConfiguration(
            productionURL: "your validation server production url",
            sandboxURL: "your validation server sandbox url",
            sessionConfiguration: .default
        )
        
        receiptValidator = SwiftyReceiptValidator(configuration: configuration, isLoggingEnabled: false)
    }
}

您的网页服务器然后将收到的响应发送到苹果的服务器进行验证

  • https://buy.itunes.apple.com/verifyReceipt
  • https://sandbox.itunes.apple.com/verifyReceipt

处理响应后,将其发送回iOS应用进行最终验证。

  • 标准配置(不推荐)

标准配置无需您的网页服务器,直接将验证请求发送到苹果的服务器。这种方法安全性不高,因此不推荐。

class SomeClass {
    let receiptValidator: SwiftyReceiptValidatorType
    
    init() {
        receiptValidator = SwiftyReceiptValidator(configuration: .standard, isLoggingEnabled: false)
    }
}

验证购买

  • 请前往您的应用程序内购买代码中的以下代理方法,您必须实现该方法。
extension SomeClass: SKPaymentTransactionObserver {
    func paymentQueue(_ queue: SKPaymentQueue, updatedTransactions transactions: [SKPaymentTransaction]) {
        transactions.forEach { transaction in
            switch transaction.transactionState {
                ...
            }
        }
    }
}

并修改 .purchased 的情况,如下所示

case .purchased:
    // Transaction is in queue, user has been charged.  Client should complete the transaction.
    let productId = transaction.payment.productIdentifier

    let validationRequest = SRVPurchaseValidationRequest(
        productId: productId,
        sharedSecret: "your shared secret setup in iTunesConnect or nil when dealing with non-subscription purchases"
    )
        
    receiptValidator.validate(validationRequest) { result in
        switch result {
        case .success(let response):
            defer {
                // IMPORTANT: Finish the transaction ONLY after validation was successful
                // if validation error e.g due to internet, the transaction will stay in pending state
                // and than can/will be resumed on next app launch
                queue.finishTransaction(transaction)
            }
            print("Receipt validation was successfull with receipt response \(response)")
            // Unlock products and/or do additional checks
        case .failure(let error):
            print("Receipt validation failed with error \(error.localizedDescription)")  
            // Inform user of error
        }
    }

在SwiftyReceiptValidator的旧版本中,我建议还为 .restored 情况添加此代码,这是不正确的。

注意:如果您的目标为iOS 13及更高版本,此方法还支持 Combine

let cancellable = receiptValidator
    .validatePublisher(for: validationRequest)
    .map { response in
        print(response)
    }
    .mapError { error in
        print(error)
    }

注意:如果您的目标为iOS 15及更高版本,此方法还支持 async/await

do {
    let response = try await receiptValidator.validate(validationRequest)
    print(response)
} catch {
    print(error)
}

验证订阅

  • 要验证您的订阅(例如在应用程序启动时),创建一个订阅验证请求并进行验证。这将搜索设备上找到的所有订阅收据。
let validationRequest = SRVSubscriptionValidationRequest(
    sharedSecret: "your shared secret setup in iTunesConnect",
    refreshLocalReceiptIfNeeded: false,
    excludeOldTransactions: false,
    now: Date()
)
receiptValidator.validate(validationRequest) { result in
    switch result {
    
    case .success(let response):
        print(response.receiptResponse) // full receipt response
        print(response.validSubscriptionReceipts) // convenience array for active subscription receipts

        // Check the validSubscriptionReceipts and unlock products accordingly 
        // or disable features if no active subscriptions are found e.g.
        
        if response.validSubscriptionReceipts.isEmpty {
           // disable subscription features etc
        } else {
           // Valid subscription receipts are sorted by latest expiry date
           // enable subscription features etc
        }
        
    case .failure(let error):
        switch error {
        case .noReceiptFoundInBundle:
             break
             // do nothing, see description below
        case .subscriptioniOS6StyleExpired(let statusCode):
            // Only returned for iOS 6 style transaction receipts for auto-renewable subscriptions.
            // This receipt is valid but the subscription has expired. 
            
            // disable subscription features 
        default:
            // do nothing or inform user of error during validation e.g UIAlertController
        }
    }
}

refreshLocalReceiptIfNeeded 设置为 true,如果没有在您的应用程序包中找到收据,则会创建一个 SKReceiptRefreshRequest

我建议以下原因始终将此标志设置为 false

  1. 创建一个 SKReceiptRefreshRequest 会在应用程序中始终显示iTunes密码提示,这可能在您的应用程序流程中不需要。
  2. 当您在应用程序启动时调用它时,您可以单独处理返回的 SRVError.noReceiptFoundInBundle 错误。
  3. 一旦用户进行应用内购买,您的应用程序包中应该始终存在一个收据。
  4. 重新安装应用程序的用户,如果他们有一个现有的订阅,应使用您应用程序中的恢复功能,这是使用应用内购买的必备要求。这将添加您的应用程序包中的收据,然后可以验证订阅。(https://developer.apple.com/documentation/storekit/skpaymentqueue/1506123-restorecompletedtransactions)。

注意:如果您的目标为iOS 13及更高版本,此方法还支持 Combine

let cancellable = receiptValidator
    .validatePublisher(for: validationRequest)
    .map { response in
        print(response)
    }
    .mapError { error in
        print(error)
    }

注意:如果您的目标为iOS 15及更高版本,此方法还支持 async/await

do {
    let response = try await receiptValidator.validate(validationRequest)
    print(response)
} catch {
    print(error)
}

检查自动续订状态

如果您想检查用户自动续订的状态,建议在了解的情况下首先检查即将到来的续订信息,然后回退到当前订阅状态。

let validationRequest = SRVSubscriptionValidationRequest(...)
receiptValidator.validate(validationRequest) { result in
    switch result {
    case .success(let response):
    
        let isAutoRenewOn: Bool
        if let pendingRenewalInfo = response.receiptResponse.pendingRenewalInfo, !pendingRenewalInfo.isEmpty {
            isAutoRenewOn = pendingRenewalInfo.contains(where: { $0.autoRenewStatus == .on })
        } else {
            isAutoRenewOn = response.validSubscriptionReceipts.contains(where: { $0.autoRenewStatus == .on })
        }
    
    case .failure(let error):
        ...
    }
}

显示入门价格

如果收据中的一个以前的订阅周期有任一键(is_trial_period 或 is_in_intro_offer_period)的值为“true”,则用户在该订阅组中不符合免费试用或入门价格的条件。 SwiftyReceiptValidator 提供了一个方便的布尔值用于这个

let validationRequest = SRVSubscriptionValidationRequest(...)
receiptValidator.validate(validationRequest) { result in
    switch result {
    case .success(let response):
        response.validSubscriptionReceipts.forEach { receipt in
            print(receipt.canShowIntroductoryPrice)
        }
    case .failure(let error):
    ...
}

测试

为了测试你的内购类,建议始终将类型协议注入到你的类中,而不是具体实现

  • 不推荐
class SomeClass {
    private let receiptValidator: SwiftyReceiptValidator
    init(receiptValidator: SwiftyReceiptValidator) { ... }
}
  • 推荐
class SomeClass {
    private let receiptValidator: SwiftyReceiptValidatorType
    init(receiptValidator: SwiftyReceiptValidatorType) { ... }
}
  • 单元测试示例
class MockReceiptValidator { }
extension MockReceiptValidator: SwiftyReceiptValidatorType { 
    // implement SwiftyReceiptValidatorType protocol methods and return mocks/fake data (see Mocking Models below)
 }

class SomeClassTests {
    func testSomething() {
        let sut = SomeClass(receiptValidator: MockReceiptValidator())
    }
}
  • 模拟模型
SRVReceiptResponse.mock()
SRVReceipt.mock()
SRVReceiptInApp.mock()
SRVPendingRenewalInfo.mock()
SRVSubscriptionValidationResponse.mock()

StoreKit 通知控制器

当你到达购买代码和 .purchased switch 语句时,StoreKit 会自动显示一个 AlertController(“感谢您的购买成功”)。这是收据验证开始的点,您可能想要显示一个自定义的加载/验证提示。我认为您无法禁用显示默认提示。

总结

根据苹果的指南,您应该始终首先连接到苹果的生产服务器,然后在需要时回退到沙盒服务器。因此,当在沙盒模式下测试时,请记住这一点,由于这一点,验证可能会稍微慢一些。

许可

SwiftyReceiptValidator 在 MIT 许可下发布。有关详细信息,请参阅 LICENSE