将 IAP 存储在 UserDefaults 中,这样用户就不需要在每次按下“可购买”按钮时恢复购买

Posted

技术标签:

【中文标题】将 IAP 存储在 UserDefaults 中,这样用户就不需要在每次按下“可购买”按钮时恢复购买【英文标题】:Storing IAP in UserDefaults so the user doesn't need to restore the purchase every time a "purchasable" button is pressed 【发布时间】:2020-05-16 17:04:31 【问题描述】:

背景:我的应用程序从一组数组中提供了许多提示。 “包”确定可以打开哪些提示。您从默认包开始,然后可以为非消耗性 IAP 购买三个额外的包(packA、packB 和 packC)。

我的目标是让用户支付一次,然后就可以在他/她喜欢的任何时候访问这些包;但是,一旦沙盒用户进行了 IAP,就会弹出一个提示 “你已经购买了这个。你想再次免费获得它吗?”。我显然不希望每次用户选择一个包时都会弹出这个。有没有办法让购买成为永久使用,而不需要不断恢复购买?

以下是我当前的代码(匿名并简化为仅基本组件):

import UIKit
import QuartzCore
import StoreKit

class ViewController: UIViewController, SKPaymentTransactionObserver 


    //MAIN SETUP SECTION XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

    let productID = "com.domain.appName.additionalPackages" 
    let defaults = UserDefaults.standard

    //I'm just putting "Various" here as a placeholder for my multiple buttons
    @IBOutlet weak var (Various): UIButton!

    override func viewDidLoad() 
        super.viewDidLoad()


        SKPaymentQueue.default().add(self) 

        //I don’t know if this actually does anything        
        func cleanUp() 
            for transaction in SKPaymentQueue.default().transactions 
                SKPaymentQueue.default().finishTransaction(transaction)
            
        

    




    //PACK SELECTION SECTION XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

    var packA = 2
    var packB = 2
    var packC = 2
    var packsUnlocked = false

    @IBAction func selectPackA(_ sender: UIButton) 
        if packsUnlocked == false 
            print("It's locked, ‘Pack A’ not enabled")
         else if packCounterA % 2 == 0 
            if SKPaymentQueue.canMakePayments()  // In App Purchase
            let paymentRequest = SKMutablePayment()
            paymentRequest.productIdentifier = productID
            SKPaymentQueue.default().add(paymentRequest)
            print("Initiating Transaction")
         else 
            print("No Purchased")
        
        promptProvider.includeA.toggle()
            packCounterA += 1
         else 
            promptProvider.includeA.toggle()
            packCounterA += 1
        
    

    @IBAction func selectPackB(_ sender: UIButton) 
        if packsUnlocked == false 
            print("It's locked, ‘Pack B’ not enabled")
         else if packCounterB % 2 == 0 
            if SKPaymentQueue.canMakePayments()  // In App Purchase
            let paymentRequest = SKMutablePayment()
            paymentRequest.productIdentifier = productID
            SKPaymentQueue.default().add(paymentRequest)
            print("Initiating Transaction")
         else 
            print("No Purchased")
        
            promptProvider.includeB.toggle()
            packCounterB += 1
         else 
            promptProvider.includeB.toggle()
            packCounterB += 1
        
    

    @IBAction func selectPackC(_ sender: UIButton) 
        if packsUnlocked == false 
            print("It's locked, ‘Pack C' not enabled")
         else if packCounterC % 2 == 0 
            if SKPaymentQueue.canMakePayments()  // In App Purchase
            let paymentRequest = SKMutablePayment()
            paymentRequest.productIdentifier = productID
            SKPaymentQueue.default().add(paymentRequest)
            print("Initiating Transaction")
         else 
            print("No Purchased")
        
            promptProvider.includeC.toggle()
            packCounterC += 1
         else 
            promptProvider.includeC.toggle()
            packCounterC += 1
        
    




    //TRANSACTION FINALIZATION SECTION XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

    func paymentQueue(_ queue: SKPaymentQueue, updatedTransactions transactions: [SKPaymentTransaction])  
        for transaction in transactions 
            guard
                transaction.transactionState != .purchasing,
                transaction.transactionState != .deferred
            else 
                //Optionally provide user feedback for pending or processing transactions
                continue
            

            if transaction.transactionState == .purchased || transaction.transactionState == .restored 
                print("Transaction Successful")
                packsUnlocked = true
                //new
                defaults.set(true, forKey: "Purchase Pack") 
                UserDefaults.standard.synchronize() 
             else if transaction.transactionState == .failed 
                print("Transaction Failed with error")
            

            //Transaction can now be safely finished
            queue.finishTransaction(transaction)
        
    




这是我的第一个应用程序,但我认为问题在于将购买保存到 UserDefaults。我对此很陌生,因此非常感谢任何帮助。

谢谢

【问题讨论】:

您在哪里存储包购买状态?钥匙扣很常见。用户只有在使用新设备或已擦除设备时才需要再次尝试购买 @Paulw11 这可能是我的问题,如果上述代码中尚未完成,我不知道如何存储每个人的购买状态。我在网上搜索过这方面的帮助,但找不到任何东西。记录/举例说明此代码的任何想法? 这取决于你。我确信在 Ray Wendlich 等网站上有 IAP 教程。将值存储在钥匙串中很常见,您可以使用 KeychainSwift 之类的框架来简化它。 您可以使用设备上的持久存储将State(购买、恢复)与相关productIdentifier 一起存储。您不应该使用 UserDefaults,因为它可以被操纵(例如有人可以将其替换为购买的,即使它不是),您可以按照上面的建议将其存储在 Keychain 上。你可以使用,paymentQueue(_:updatedTransactions:)delegate。 @omerfarukozturk 我实际上对 userDefaults 的限制很好,因为我真的只是想在截止日期前将这个产品推向市场,然后在它上市后改进它。您发送给我的链接非常有帮助,但我在将其使用 UserDefaults 的方法与我的方法区分开来时遇到问题。看来我会做文档告诉我做的所有事情......关于[坚持我的交易](developer.apple.com/documentation/storekit/in-app_purchase/…)的任何建议? 【参考方案1】:

基础知识

购买完成后,您可以通过Bundle.main.appStoreReceiptURL 访问收据。您可以使用该收据并针对 App Store 进行验证,以防止有人使用假收据。

验证回复会告诉您收据是合法的并且没有被退款。它还会告诉您订阅仍然有效以及何时到期。

您可以在客户端上执行所有这些操作,但最好将收据存储在服务器上,这样您就可以在终端验证它,而不是将其留给客户端进行潜在的操作。

不要调用 App Store 服务器 verifyReceipt 端点 你的应用程序。您无法在用户设备之间建立可信连接 和 App Store 直接,因为你不控制任何一端 这种联系,这使得它容易受到中间人的影响 攻击。

您可以在docs 中找到更多信息,包括代码示例。另外你可以设置hooks,这样当订阅状态发生变化时,你的服务器会主动得到通知。

概念验证

如果您不关心上述安全隐患而只想做概念验证,您可以在应用中执行此操作并检查用户是否已经购买:

    Bundle.main.appStoreReceiptURL?.path获取收据 如here 所述,使用 App Store 服务器验证收据;请注意,沙盒应用商店和生产应用商店的验证 URL 不同 从验证响应中获取 product_idexpires_date 以了解购买了哪些产品以及是否已过期

请记住,如果用户卸载并重新安装应用程序,收据不会存储在设备上。然后,用户必须在应用程序从 App Store 服务器下载收据时恢复购买。当您按下您可能从其他应用程序中熟悉的“恢复购买”按钮时,就会发生这种情况。只需使用SKReceiptRefreshRequest 即可完成恢复。

【讨论】:

谢谢@Manuel - 我实际上对使用 UserDefaults 的安全限制很好 - 所以我的主要问题实际上只是让我的购买持久化。我是否需要使用现有代码做更多的收据,还是您认为它只是某个地方的问题阻止了购买的持续? 我为答案添加了一个简单的概念证明指南。

以上是关于将 IAP 存储在 UserDefaults 中,这样用户就不需要在每次按下“可购买”按钮时恢复购买的主要内容,如果未能解决你的问题,请参考以下文章

如何将 NSArray 存储在 Userdefaults 中?

如何将 UIView 存储在 CoreData 或 UserDefaults 中

如何将 [String: UIImage] 类型的字典存储到 CoreData/UserDefaults 中?

在 UserDefaults 中存储字典

如何将计算结果存储在 UserDefaults 中以显示在“结果”视图控制器中 [重复]

在 userdefaults 中存储背景图像