【问题标题】:Getting Original Purchase Version Downloaded By User获取用户下载的原始购买版本
【发布时间】:2019-02-04 12:35:16
【问题描述】:

所以我正在将我的应用从付费更改为免费,我希望让付费客户继续使用高级功能。这样做的一种方法是检查他们最初购买的应用程序版本,看看它是否是付费版本,然后只给他们高级功能,但我只能找到用户当前版本的应用程序而不是原始购买的版本.

我一直在阅读,也许这可能与收据验证有关,但如果有办法让用户获得应用程序的原始购买版本,请有人帮忙。

这是我用来获取当前版本的代码,而不是最初购买的代码。

let version : String! = Bundle.main.object(forInfoDictionaryKey: "CFBundleShortVersionString") as? String
print(version)

谢谢

【问题讨论】:

  • 是的,确实,通过收据验证,您应该看到用户的所有购买,并在验证后最终解锁您想要的功能。检查ASN.1 Field Type 17,如apple docs specify 在JSON文件中,此键的值是一个数组,其中包含基于输入base-64收据中存在的应用内购买交易的所有应用内购买收据-数据
  • 我将如何实现这个? @A_C
  • 老实说,这对我来说也是个问题。我将在答案中分享代码,因为评论太长了。我只是希望不要投反对票,因为我不是 100% 确定这是正确的过程:D

标签: swift version receipt-validation


【解决方案1】:

正如我在 cmets 中所写,我不确定这是否是正确的流程,但对于我制作的应用,我使用以下代码检查收据:

有任何疑问请参考the docs我也关注了。

还需要注意的是,Apple 不鼓励直接通过 AppStore 服务器验证(因为无法验证身份,这可能导致中间人攻击)

使用受信任的服务器与 App Store 进行通信。使用您自己的服务器可让您将应用设计为仅识别和信任您的服务器,并确保您的服务器与 App Store 服务器连接。无法直接在用户设备和 App Store 之间建立受信任的连接,因为您无法控制该连接的任何一端,因此很容易受到中间人攻击。

但是,如果可以帮助您,这里有两个 Apple 端点(调试/生产)。

    #if DEBUG
    private let appStoreValidationURL = URL(string: "https://sandbox.itunes.apple.com/verifyReceipt")!
    #else
    private let appStoreValidationURL = URL(string: "https://buy.itunes.apple.com/verifyReceipt")!
    #endif

同时,关于您需要传递收据的应用程序的SharedSecret,您可以找到有用的信息here

  1. 检索收据。
private func loadReceipt() throws -> Data {
        guard let url = Bundle.main.appStoreReceiptURL else {
            throw ReceiptValidationError.noReceiptData
        }

        do {
            let data = try Data(contentsOf: url)
            return data
        } catch {
            print("Error loading receipt data: \(error.localizedDescription)")
            throw ReceiptValidationError.noReceiptData
        }
    }
  1. 然后您可以使用 JSON 格式读取内容
[...]
 // Handle the try. I skipped that to make easier to read 
 let data = try! loadReceipt()
 let base64String = data.base64EncodedString(options: [])

 // Encode data in JSON
 let content: [String : Any] = ["receipt-data" : base64String,
                                       "password" : sharedSecret,
                                       "exclude-old-transactions" : true]

  1. 将您的请求收据发送到 Apple 服务器进行验证。
private func validateLastReceipt(_ data: Data) {

        let base64String = data.base64EncodedString(options: [])

        // Encode data in JSON
        let content: [String : Any] = ["receipt-data" : base64String,
                                       "password" : sharedSecret,
                                       "exclude-old-transactions" : false]
        let json = try! JSONSerialization.data(withJSONObject: content, options: [])

        // build request
        let storeURL = self.appStoreValidationURL

        var request = URLRequest(url: storeURL)
        request.httpMethod = "POST"
        request.httpBody = json

        // Make request to app store

        URLSession.shared.dataTask(with: request) { (data, res, error) in
            guard error == nil, let data = data else {
                self.delegate?.validator(self, didFinishValidateWith: error!)
                return
            }

            do {
                let decoder = JSONDecoder()
                let response = try decoder.decode(ReceiptAppStoreResponse.self, from: data)                                
            } catch {
                // Handle error
            }

            }.resume()
    }

这里是我构建的 Decodables 结构。 您将在此处找到检查用户购买商品所需的所有信息!

private struct ReceiptAppStoreResponse: Decodable {
    /// Either 0 if the receipt is valid, or one of the error codes listed in Table 2-1.
    ///
    /// For iOS 6 style transaction receipts, the status code reflects the status of the specific transaction’s receipt.
    ///
    /// For iOS 7 style app receipts, the status code is reflects the status of the app receipt as a whole. For example, if you send a valid app receipt that contains an expired subscription, the response is 0 because the receipt as a whole is valid.
    let status: Int?

    /// A JSON representation of the receipt that was sent for verification.
//    let receipt: String?

    /// Only returned for receipts containing auto-renewable subscriptions. For iOS 6 style transaction receipts,
    /// this is the base-64 encoded receipt for the most recent renewal. For iOS 7 style app receipts, this is the latest
    /// base-64 encoded app receipt.
    let latestReceipt: String?

    /// Only returned for receipts containing auto-renewable subscriptions. For iOS 6 style transaction receipts,
    /// this is the JSON representation of the receipt for the most recent renewal. For iOS 7 style app receipts,
    /// the value of this key is an array containing all in-app purchase transactions.
    /// This excludes transactions for a consumable product that have been marked as finished by your app.
    let latestReceiptInfo: [ReceiptInfo]?

    /// Only returned for iOS 6 style transaction receipts, for an auto-renewable subscription.
    /// The JSON representation of the receipt for the expired subscription.
    //    let latestExpiredReceiptInfo: String?

    /// Only returned for iOS 7 style app receipts containing auto-renewable subscriptions.
    /// In the JSON file, the value of this key is an array where each element contains the pending renewal information
    /// for each auto-renewable subscription identified by the Product Identifier.
    /// A pending renewal may refer to a renewal that is scheduled in the future or a renewal that failed
    /// in the past for some reason.
    //    let pendingRenewalInfo: String?

    /// Retry validation for this receipt. Only applicable to status codes 21100-21199
    //    let isRetryable: Bool?

    enum CodingKeys: String, CodingKey {
        case status
//        case receipt
        case latestReceipt = "latest_receipt"
        case latestReceiptInfo = "latest_receipt_info"
        //        case latestExpiredReceiptInfo = "latest_expired_receipt_info"
        //        case pendingRenewalInfo = "pending_renewal_info"
        //        case isRetryable = "is-retryable"
    }

}

struct ReceiptInfo: Decodable {

    let originalTransactionID: String?
    let productID: String?

    let expiresDateMS: String?

    let originalPurchaseDateMS: String?

    let isTrialPeriod: String?
    let isInIntroOfferPeriod: String?

    let purchaseDateMS: String?

    enum CodingKeys: String, CodingKey {
        case originalTransactionID = "original_transaction_id"
        case productID = "product_id"

        case expiresDateMS = "expires_date_ms"

        case originalPurchaseDateMS = "original_purchase_date_ms"

        case isTrialPeriod = "is_trial_period"
        case isInIntroOfferPeriod = "is_in_intro_offer_period"

        case purchaseDateMS = "purchase_date_ms"
    }

    func getExpireDate() -> Date? {
        let nf = NumberFormatter()
        guard let expDateString = self.expiresDateMS, let expDateValue = nf.number(from: expDateString) else {
            return nil
        }

        /// It's expressed as milliseconds since 1970!!!
        let date = Date(timeIntervalSince1970: expDateValue.doubleValue / 1000)

        return date

    }

希望对您有所帮助! :)

【讨论】:

  • 非常感谢您的深入解释。一个快速的问题。在 2. 你说“//处理尝试。我跳过了它以便于阅读”。你这是什么意思? @A_C
  • 理论上如果抛出函数返回(或者,更好的是抛出)一个错误,它由 catch 部分处理。我跳过了它并使用了try! 的强制尝试,但是如果抛出函数抛出错误应用程序崩溃,则这样做。检查我在第 1 点中的表现。
猜你喜欢
  • 2016-09-27
  • 2014-06-16
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2012-09-24
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多