Swift 如何处理自动续订订阅收据和验证
Swift How to handle Auto-renewable Subscription receipt and validation
我正在 swift 测试自动更新应用内购买,我发现我的代码有一些奇怪的问题。
我正在沙盒环境中测试这些功能
- 用户可以购买一个月、一年的自动续订订阅或永久许可
- 应用程序应该在用户每次打开应用程序时检查订阅是否仍然有效,如果不是,则锁定所有高级功能
- 用户能够恢复购买的计划,应用程序应该得到以前购买的类型,即。一个月、一年或永久。
经过长时间的教程研究,我仍然对验证感到困惑
- 我看到有两种验证收据的方法,一种是在本地,另一种是在服务器上。
但是我没有服务器,是不是就只能在本地验证了
- 每次自动续订订阅到期时,本地收据不会更新,所以当我重新打开应用程序时,我收到订阅到期警报(我自己定义的验证验证方法),当我点击恢复按钮,应用恢复成功并更新收据
- 经过6次手动恢复并刷新收据(沙盒用户只能更新6次),当我点击恢复按钮时,部分交易== .purchased被调用,我的应用程序解锁了高级功能,然而,当我重新打开我的应用程序时,我的应用程序会提醒订阅已过期,这是应该的。
我的核心问题是每次打开应用程序时如何检查与Apple的订阅验证,我没有服务器,我不知道为什么收据没有自动刷新
这是我的部分代码,我在打开应用程序时调用了 checkUserSubsriptionStatus(),我正在使用 TPInAppReceipt 库
class InAppPurchaseManager {
static var shared = InAppPurchaseManager()
init() {
}
public func getUserPurchaseType() -> PurchaseType {
if let receipt = try? InAppReceipt.localReceipt() {
var purchaseType: PurchaseType = .none
if let purchase = receipt.lastAutoRenewableSubscriptionPurchase(ofProductIdentifier: PurchaseType.oneMonth.productID) {
purchaseType = .oneMonth
}
if let purchase = receipt.lastAutoRenewableSubscriptionPurchase(ofProductIdentifier: PurchaseType.oneYear.productID) {
purchaseType = .oneYear
}
if receipt.containsPurchase(ofProductIdentifier: PurchaseType.permanent.productID) {
purchaseType = .permanent
}
return purchaseType
} else {
print("Receipt not found")
return .none
}
}
public func restorePurchase(in viewController: SKPaymentTransactionObserver) {
SKPaymentQueue.default().add(viewController)
if SKPaymentQueue.canMakePayments() {
SKPaymentQueue.default().restoreCompletedTransactions()
} else {
self.userIsNotAbleToPurchase()
}
}
public func checkUserSubsriptionStatus() {
DispatchQueue.main.async {
if let receipt = try? InAppReceipt.localReceipt() {
self.checkUserPermanentSubsriptionStatus(with: receipt)
}
}
}
private func checkUserPermanentSubsriptionStatus(with receipt: InAppReceipt) {
if let receipt = try? InAppReceipt.localReceipt() { //Check permsnent subscription
if receipt.containsPurchase(ofProductIdentifier: PurchaseType.permanent.productID) {
print("User has permament permission")
if !AppEngine.shared.currentUser.isVip {
self.updateAfterAppPurchased(withType: .permanent)
}
} else {
self.checkUserAutoRenewableSubsrption(with: receipt)
}
}
}
private func checkUserAutoRenewableSubsrption(with receipt: InAppReceipt) {
if receipt.hasActiveAutoRenewablePurchases {
print("Subsription still valid")
if !AppEngine.shared.currentUser.isVip {
let purchaseType = InAppPurchaseManager.shared.getUserPurchaseType()
updateAfterAppPurchased(withType: purchaseType)
}
} else {
print("Subsription expired")
if AppEngine.shared.currentUser.isVip {
self.subsrptionCheckFailed()
}
}
}
private func updateAfterAppPurchased(withType purchaseType: PurchaseType) {
AppEngine.shared.currentUser.purchasedType = purchaseType
AppEngine.shared.currentUser.energy += 5
AppEngine.shared.userSetting.hasViewedEnergyUpdate = false
AppEngine.shared.saveUser()
AppEngine.shared.notifyAllUIObservers()
}
public func updateAfterEnergyPurchased() {
AppEngine.shared.currentUser.energy += 3
AppEngine.shared.saveUser()
AppEngine.shared.notifyAllUIObservers()
}
public func purchaseApp(with purchaseType: PurchaseType, in viewController: SKPaymentTransactionObserver) {
SKPaymentQueue.default().add(viewController)
if SKPaymentQueue.canMakePayments() {
let paymentRequest = SKMutablePayment()
paymentRequest.productIdentifier = purchaseType.productID
SKPaymentQueue.default().add(paymentRequest)
} else {
self.userIsNotAbleToPurchase()
}
}
public func purchaseEnergy(in viewController: SKPaymentTransactionObserver) {
SKPaymentQueue.default().add(viewController)
let productID = "com.crazycat.Reborn.threePointOfEnergy"
if SKPaymentQueue.canMakePayments() {
let paymentRequest = SKMutablePayment()
paymentRequest.productIdentifier = productID
SKPaymentQueue.default().add(paymentRequest)
} else {
self.userIsNotAbleToPurchase()
}
}
}
每次应用启动时调用AppDelegate中的方法获取回执。
getAppReceipt(forTransaction: nil)
现在,下面是所需的方法:
func getAppReceipt(forTransaction transaction: SKPaymentTransaction?) {
guard let receiptURL = receiptURL else { /* receiptURL is nil, it would be very weird to end up here */ return }
do {
let receipt = try Data(contentsOf: receiptURL)
receiptValidation(receiptData: receipt, transaction: transaction)
} catch {
// there is no app receipt, don't panic, ask apple to refresh it
let appReceiptRefreshRequest = SKReceiptRefreshRequest(receiptProperties: nil)
appReceiptRefreshRequest.delegate = self
appReceiptRefreshRequest.start()
// If all goes well control will land in the requestDidFinish() delegate method.
// If something bad happens control will land in didFailWithError.
}
}
这是 receiptValidation 方法:
func receiptValidation(receiptData: Data?, transaction: SKPaymentTransaction?) {
guard let receiptString = receiptData?.base64EncodedString(options: NSData.Base64EncodingOptions(rawValue: 0)) else { return }
verify_in_app_receipt(with_receipt_string: receiptString, transaction: transaction)
}
接下来是验证收据和获取订阅到期日期的最后一个方法:
func verify_in_app_receipt(with_receipt_string receiptString: String, transaction: SKPaymentTransaction?) {
let params: [String: Any] = ["receipt-data": receiptString,
"password": "USE YOUR PASSWORD GENERATED FROM ITUNES",
"exclude-old-transactions": true]
// Below are the url's used for in app receipt validation
//appIsInDevelopment ? "https://sandbox.itunes.apple.com/verifyReceipt" : "https://buy.itunes.apple.com/verifyReceipt"
super.startService(apiType: .verify_in_app_receipt, parameters: params, files: [], modelType: SubscriptionReceipt.self) { (result) in
switch result {
case .Success(let receipt):
if let receipt = receipt {
print("Receipt is: \(receipt)")
if let _ = receipt.latest_receipt, let receiptArr = receipt.latest_receipt_info {
var expiryDate: Date? = nil
for latestReceipt in receiptArr {
if let dateInMilliseconds = latestReceipt.expires_date_ms, let product_id = latestReceipt.product_id {
let date = Date(timeIntervalSince1970: dateInMilliseconds / 1000)
if date >= Date() {
// Premium is valid
}
}
}
if expiryDate == nil {
// Premium is not purchased or is expired
}
}
}
case .Error(let message):
print("Error in api is: \(message)")
}
}
}
如果您无法使用服务器,则需要在本地进行验证。由于您已经包含了 TPInAppReceipt 库,因此这相对容易。
要检查用户是否有激活的高级产品及其类型,您可以使用以下代码:
// Get all active purchases which are convertible to `PurchaseType`.
let premiumPurchases = receipt.activeAutoRenewableSubscriptionPurchases.filter({ PurchaseType(rawValue: [=10=].productIdentifier) != nil })
// It depends on how your premium access works, but if it doesn't matter what kind of premium the user has, it is enough to take one of the available active premium products.
// Note: with the possibility to share subscriptions via family sharing, the receipt can contain multiple active subscriptions.
guard let product = premiumPurchases.first else {
// User has no active premium product => lock all premium features
return
}
// To be safe you can use a "guard" or a "if let", but since we filtered for products conforming to PurchaseType, this shouldn't fail
let purchaseType = PurchaseType(rawValue: product.productIdentifier)!
// => Setup app corresponding to active premium product type
我在您的代码中注意到的一点可能会导致问题,那就是您不断添加新的 SKPaymentTransactionObserver
。您应该有一个 class 符合 SKPaymentTransactionObserver
并且只在应用程序启动时添加一次,而不是在每次 public 调用时添加。另外,当你不再需要它时,你需要删除它(如果你只创建它一次,你会在你的 class 的 deinit
中执行它,符合观察者协议。
我想这就是第 2 点的原因。
从技术上讲,第 3 点中描述的行为是正确的,因为您使用的方法要求支付队列恢复所有以前完成的购买(参见 here)。
Apple 声明 restoreCompletedTransactions()
应仅用于以下情况(参见 here):
- 如果您使用 Apple 托管的内容,恢复已完成的交易会为您的应用提供用于下载内容的交易对象。
- 如果您需要支持 iOS 早于 iOS 7 的版本,应用收据不可用,请恢复已完成的交易。
- 如果您的应用使用非续订订阅,您的应用将负责恢复过程。
对于您的情况,建议使用SKReceiptRefreshRequest
,它要求更新当前收据。
我正在 swift 测试自动更新应用内购买,我发现我的代码有一些奇怪的问题。
我正在沙盒环境中测试这些功能
- 用户可以购买一个月、一年的自动续订订阅或永久许可
- 应用程序应该在用户每次打开应用程序时检查订阅是否仍然有效,如果不是,则锁定所有高级功能
- 用户能够恢复购买的计划,应用程序应该得到以前购买的类型,即。一个月、一年或永久。
经过长时间的教程研究,我仍然对验证感到困惑
- 我看到有两种验证收据的方法,一种是在本地,另一种是在服务器上。 但是我没有服务器,是不是就只能在本地验证了
- 每次自动续订订阅到期时,本地收据不会更新,所以当我重新打开应用程序时,我收到订阅到期警报(我自己定义的验证验证方法),当我点击恢复按钮,应用恢复成功并更新收据
- 经过6次手动恢复并刷新收据(沙盒用户只能更新6次),当我点击恢复按钮时,部分交易== .purchased被调用,我的应用程序解锁了高级功能,然而,当我重新打开我的应用程序时,我的应用程序会提醒订阅已过期,这是应该的。
我的核心问题是每次打开应用程序时如何检查与Apple的订阅验证,我没有服务器,我不知道为什么收据没有自动刷新
这是我的部分代码,我在打开应用程序时调用了 checkUserSubsriptionStatus(),我正在使用 TPInAppReceipt 库
class InAppPurchaseManager {
static var shared = InAppPurchaseManager()
init() {
}
public func getUserPurchaseType() -> PurchaseType {
if let receipt = try? InAppReceipt.localReceipt() {
var purchaseType: PurchaseType = .none
if let purchase = receipt.lastAutoRenewableSubscriptionPurchase(ofProductIdentifier: PurchaseType.oneMonth.productID) {
purchaseType = .oneMonth
}
if let purchase = receipt.lastAutoRenewableSubscriptionPurchase(ofProductIdentifier: PurchaseType.oneYear.productID) {
purchaseType = .oneYear
}
if receipt.containsPurchase(ofProductIdentifier: PurchaseType.permanent.productID) {
purchaseType = .permanent
}
return purchaseType
} else {
print("Receipt not found")
return .none
}
}
public func restorePurchase(in viewController: SKPaymentTransactionObserver) {
SKPaymentQueue.default().add(viewController)
if SKPaymentQueue.canMakePayments() {
SKPaymentQueue.default().restoreCompletedTransactions()
} else {
self.userIsNotAbleToPurchase()
}
}
public func checkUserSubsriptionStatus() {
DispatchQueue.main.async {
if let receipt = try? InAppReceipt.localReceipt() {
self.checkUserPermanentSubsriptionStatus(with: receipt)
}
}
}
private func checkUserPermanentSubsriptionStatus(with receipt: InAppReceipt) {
if let receipt = try? InAppReceipt.localReceipt() { //Check permsnent subscription
if receipt.containsPurchase(ofProductIdentifier: PurchaseType.permanent.productID) {
print("User has permament permission")
if !AppEngine.shared.currentUser.isVip {
self.updateAfterAppPurchased(withType: .permanent)
}
} else {
self.checkUserAutoRenewableSubsrption(with: receipt)
}
}
}
private func checkUserAutoRenewableSubsrption(with receipt: InAppReceipt) {
if receipt.hasActiveAutoRenewablePurchases {
print("Subsription still valid")
if !AppEngine.shared.currentUser.isVip {
let purchaseType = InAppPurchaseManager.shared.getUserPurchaseType()
updateAfterAppPurchased(withType: purchaseType)
}
} else {
print("Subsription expired")
if AppEngine.shared.currentUser.isVip {
self.subsrptionCheckFailed()
}
}
}
private func updateAfterAppPurchased(withType purchaseType: PurchaseType) {
AppEngine.shared.currentUser.purchasedType = purchaseType
AppEngine.shared.currentUser.energy += 5
AppEngine.shared.userSetting.hasViewedEnergyUpdate = false
AppEngine.shared.saveUser()
AppEngine.shared.notifyAllUIObservers()
}
public func updateAfterEnergyPurchased() {
AppEngine.shared.currentUser.energy += 3
AppEngine.shared.saveUser()
AppEngine.shared.notifyAllUIObservers()
}
public func purchaseApp(with purchaseType: PurchaseType, in viewController: SKPaymentTransactionObserver) {
SKPaymentQueue.default().add(viewController)
if SKPaymentQueue.canMakePayments() {
let paymentRequest = SKMutablePayment()
paymentRequest.productIdentifier = purchaseType.productID
SKPaymentQueue.default().add(paymentRequest)
} else {
self.userIsNotAbleToPurchase()
}
}
public func purchaseEnergy(in viewController: SKPaymentTransactionObserver) {
SKPaymentQueue.default().add(viewController)
let productID = "com.crazycat.Reborn.threePointOfEnergy"
if SKPaymentQueue.canMakePayments() {
let paymentRequest = SKMutablePayment()
paymentRequest.productIdentifier = productID
SKPaymentQueue.default().add(paymentRequest)
} else {
self.userIsNotAbleToPurchase()
}
}
}
每次应用启动时调用AppDelegate中的方法获取回执。
getAppReceipt(forTransaction: nil)
现在,下面是所需的方法:
func getAppReceipt(forTransaction transaction: SKPaymentTransaction?) {
guard let receiptURL = receiptURL else { /* receiptURL is nil, it would be very weird to end up here */ return }
do {
let receipt = try Data(contentsOf: receiptURL)
receiptValidation(receiptData: receipt, transaction: transaction)
} catch {
// there is no app receipt, don't panic, ask apple to refresh it
let appReceiptRefreshRequest = SKReceiptRefreshRequest(receiptProperties: nil)
appReceiptRefreshRequest.delegate = self
appReceiptRefreshRequest.start()
// If all goes well control will land in the requestDidFinish() delegate method.
// If something bad happens control will land in didFailWithError.
}
}
这是 receiptValidation 方法:
func receiptValidation(receiptData: Data?, transaction: SKPaymentTransaction?) {
guard let receiptString = receiptData?.base64EncodedString(options: NSData.Base64EncodingOptions(rawValue: 0)) else { return }
verify_in_app_receipt(with_receipt_string: receiptString, transaction: transaction)
}
接下来是验证收据和获取订阅到期日期的最后一个方法:
func verify_in_app_receipt(with_receipt_string receiptString: String, transaction: SKPaymentTransaction?) {
let params: [String: Any] = ["receipt-data": receiptString,
"password": "USE YOUR PASSWORD GENERATED FROM ITUNES",
"exclude-old-transactions": true]
// Below are the url's used for in app receipt validation
//appIsInDevelopment ? "https://sandbox.itunes.apple.com/verifyReceipt" : "https://buy.itunes.apple.com/verifyReceipt"
super.startService(apiType: .verify_in_app_receipt, parameters: params, files: [], modelType: SubscriptionReceipt.self) { (result) in
switch result {
case .Success(let receipt):
if let receipt = receipt {
print("Receipt is: \(receipt)")
if let _ = receipt.latest_receipt, let receiptArr = receipt.latest_receipt_info {
var expiryDate: Date? = nil
for latestReceipt in receiptArr {
if let dateInMilliseconds = latestReceipt.expires_date_ms, let product_id = latestReceipt.product_id {
let date = Date(timeIntervalSince1970: dateInMilliseconds / 1000)
if date >= Date() {
// Premium is valid
}
}
}
if expiryDate == nil {
// Premium is not purchased or is expired
}
}
}
case .Error(let message):
print("Error in api is: \(message)")
}
}
}
如果您无法使用服务器,则需要在本地进行验证。由于您已经包含了 TPInAppReceipt 库,因此这相对容易。
要检查用户是否有激活的高级产品及其类型,您可以使用以下代码:
// Get all active purchases which are convertible to `PurchaseType`.
let premiumPurchases = receipt.activeAutoRenewableSubscriptionPurchases.filter({ PurchaseType(rawValue: [=10=].productIdentifier) != nil })
// It depends on how your premium access works, but if it doesn't matter what kind of premium the user has, it is enough to take one of the available active premium products.
// Note: with the possibility to share subscriptions via family sharing, the receipt can contain multiple active subscriptions.
guard let product = premiumPurchases.first else {
// User has no active premium product => lock all premium features
return
}
// To be safe you can use a "guard" or a "if let", but since we filtered for products conforming to PurchaseType, this shouldn't fail
let purchaseType = PurchaseType(rawValue: product.productIdentifier)!
// => Setup app corresponding to active premium product type
我在您的代码中注意到的一点可能会导致问题,那就是您不断添加新的 SKPaymentTransactionObserver
。您应该有一个 class 符合 SKPaymentTransactionObserver
并且只在应用程序启动时添加一次,而不是在每次 public 调用时添加。另外,当你不再需要它时,你需要删除它(如果你只创建它一次,你会在你的 class 的 deinit
中执行它,符合观察者协议。
我想这就是第 2 点的原因。
从技术上讲,第 3 点中描述的行为是正确的,因为您使用的方法要求支付队列恢复所有以前完成的购买(参见 here)。
Apple 声明 restoreCompletedTransactions()
应仅用于以下情况(参见 here):
- 如果您使用 Apple 托管的内容,恢复已完成的交易会为您的应用提供用于下载内容的交易对象。
- 如果您需要支持 iOS 早于 iOS 7 的版本,应用收据不可用,请恢复已完成的交易。
- 如果您的应用使用非续订订阅,您的应用将负责恢复过程。
对于您的情况,建议使用SKReceiptRefreshRequest
,它要求更新当前收据。