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
。
- 创建一个
SKReceiptRefreshRequest
会在应用程序中始终显示iTunes密码提示,这可能在您的应用程序流程中不需要。 - 当您在应用程序启动时调用它时,您可以单独处理返回的
SRVError.noReceiptFoundInBundle
错误。 - 一旦用户进行应用内购买,您的应用程序包中应该始终存在一个收据。
- 重新安装应用程序的用户,如果他们有一个现有的订阅,应使用您应用程序中的恢复功能,这是使用应用内购买的必备要求。这将添加您的应用程序包中的收据,然后可以验证订阅。(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。