Получение исходной версии покупки, загруженной пользователем

поэтому я меняю свое приложение с платного на бесплатное и хочу оставить платных клиентов с премиальными функциями. Способ сделать это состоял в том, чтобы просто проверить их первоначальную версию покупки приложения и посмотреть, была ли это платная версия, а затем просто предоставить им премиальные функции, но я могу найти только текущую версию приложения пользователей вместо исходной купленной версии. .

Я читал, и, возможно, это может быть связано с проверкой квитанции, но если бы был способ получить оригинальную версию приложения для пользователей, не мог бы кто-нибудь помочь.

Это мой код, который я использую, чтобы получить их текущую версию, а не исходную версию покупки.

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

Спасибо


person Peter Dutton    schedule 04.02.2019    source источник
comment
Да, действительно, при проверке квитанции вы должны увидеть все покупки пользователя и в конечном итоге разблокировать нужную функцию после ее проверки. Проверьте ASN.1 Field Type 17, как документы Apple указывают В файле JSON значение этого ключа представляет собой массив, содержащий все квитанции о покупках в приложении на основе транзакций о покупках в приложении, присутствующих во входных данных. данные квитанции base-64   -  person GrizzlyBear    schedule 04.02.2019
comment
и как мне реализовать это? @A_C   -  person Peter Dutton    schedule 04.02.2019
comment
Честно говоря, это было проблематично и для меня. Я поделюсь кодом в ответе, потому что он слишком длинный для комментария. Я просто надеюсь, что не получу отрицательных голосов, потому что я не уверен на 100%, что это правильный процесс: D   -  person GrizzlyBear    schedule 04.02.2019


Ответы (1)


Как я писал в комментариях, я не уверен, что это правильный процесс, но для приложения, которое я сделал, я проверяю квитанцию ​​​​со следующим кодом:

В случае возникновения сомнений обратитесь к документация, которой я также следовал.

Также важно отметить, что Apple не рекомендует проводить прямую проверку через серверы AppStore (поскольку личность не может быть проверена, и это может привести к атакам «человек посередине»)

Используйте доверенный сервер для связи с 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 вашего приложения, которое вам нужно передать вместе с квитанцией, вы можете найти полезную информацию здесь.

  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

    }

Надеюсь, поможет! :)

person GrizzlyBear    schedule 04.02.2019
comment
Большое спасибо за это в глубоком объяснении. Один быстрый вопрос. в 2. вы сказали // Обработайте попытку. Я пропустил это, чтобы было легче читать. Что ты этим имеешь ввиду? @A_C - person Peter Dutton; 05.02.2019
comment
Теоретически, если бросающая функция возвращает (или, лучше, выбрасывает) ошибку, она обрабатывается частью catch. Я пропустил это и использовал принудительную попытку с try!, но при этом, если функция метания выдает ошибку, приложение вылетает. Проверьте, как я сделал в пункте 1. - person GrizzlyBear; 05.02.2019