123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187 |
- //
- // InAppReceipt.swift
- // SwiftyStoreKit
- //
- // Created by phimage on 22/12/15.
- // Copyright (c) 2015 Andrea Bizzotto (bizz84@gmail.com)
- //
- // Permission is hereby granted, free of charge, to any person obtaining a copy
- // of this software and associated documentation files (the "Software"), to deal
- // in the Software without restriction, including without limitation the rights
- // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
- // copies of the Software, and to permit persons to whom the Software is
- // furnished to do so, subject to the following conditions:
- //
- // The above copyright notice and this permission notice shall be included in
- // all copies or substantial portions of the Software.
- //
- // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
- // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
- // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
- // THE SOFTWARE.
- import Foundation
- // MARK - receipt mangement
- internal class InAppReceipt {
- static var appStoreReceiptUrl: URL? {
- return Bundle.main.appStoreReceiptURL
- }
- static var appStoreReceiptData: Data? {
- guard let receiptDataURL = appStoreReceiptUrl, let data = try? Data(contentsOf: receiptDataURL) else {
- return nil
- }
- return data
- }
- // The base64 encoded receipt data.
- static var appStoreReceiptBase64Encoded: String? {
- return appStoreReceiptData?.base64EncodedString(options: [])
- }
- // https://developer.apple.com/library/ios/releasenotes/General/ValidateAppStoreReceipt/Chapters/ValidateRemotely.html
- /**
- * - Parameter receiptVerifyURL: receipt verify url (default: Production)
- * - Parameter password: Only used for receipts that contain auto-renewable subscriptions. Your app’s shared secret (a hexadecimal string).
- * - Parameter session: the session used to make remote call.
- * - Parameter completion: handler for result
- */
- class func verify(using validator: ReceiptValidator,
- password autoRenewPassword: String? = nil,
- completion: @escaping (VerifyReceiptResult) -> Void) {
- // If no receipt is present, validation fails.
- guard let base64EncodedString = appStoreReceiptBase64Encoded else {
- completion(.error(error: .noReceiptData))
- return
- }
- validator.validate(receipt: base64EncodedString, password: autoRenewPassword, completion: completion)
- }
- /**
- * Verify the purchase of a Consumable or NonConsumable product in a receipt
- * - Parameter productId: the product id of the purchase to verify
- * - Parameter inReceipt: the receipt to use for looking up the purchase
- * - return: either notPurchased or purchased
- */
- class func verifyPurchase(
- productId: String,
- inReceipt receipt: ReceiptInfo
- ) -> VerifyPurchaseResult {
- // Get receipts info for the product
- let receipts = receipt["receipt"]?["in_app"] as? [ReceiptInfo]
- let receiptsInfo = filterReceiptsInfo(receipts: receipts, withProductId: productId)
- // Verify that at least one receipt has the right product id
- return receiptsInfo.count >= 1 ? .purchased : .notPurchased
- }
- /**
- * Verify the purchase of a subscription (auto-renewable, free or non-renewing) in a receipt. This method extracts all transactions mathing the given productId and sorts them by date in descending order, then compares the first transaction expiry date against the validUntil value.
- * - parameter type: .autoRenewable or .nonRenewing(duration)
- * - Parameter productId: the product id of the purchase to verify
- * - Parameter inReceipt: the receipt to use for looking up the subscription
- * - Parameter validUntil: date to check against the expiry date of the subscription. If nil, no verification
- * - Parameter validDuration: the duration of the subscription. Only required for non-renewable subscription.
- * - return: either NotPurchased or Purchased / Expired with the expiry date found in the receipt
- */
- class func verifySubscription(
- type: SubscriptionType,
- productId: String,
- inReceipt receipt: ReceiptInfo,
- validUntil date: Date = Date()
- ) -> VerifySubscriptionResult {
- // Verify that at least one receipt has the right product id
- // The values of the latest_receipt and latest_receipt_info keys are useful when checking whether an auto-renewable subscription is currently active. By providing any transaction receipt for the subscription and checking these values, you can get information about the currently-active subscription period. If the receipt being validated is for the latest renewal, the value for latest_receipt is the same as receipt-data (in the request) and the value for latest_receipt_info is the same as receipt.
- let (receipts, duration) = getReceiptsAndDuration(for: type, inReceipt: receipt)
- let receiptsInfo = filterReceiptsInfo(receipts: receipts, withProductId: productId)
- let nonCancelledReceiptsInfo = receiptsInfo.filter { receipt in receipt["cancellation_date"] == nil }
- if nonCancelledReceiptsInfo.count == 0 {
- return .notPurchased
- }
- let receiptDate = getReceiptRequestDate(inReceipt: receipt) ?? date
- // Return the expires dates sorted desc
- let expiryDateValues = nonCancelledReceiptsInfo
- .flatMap { (receipt) -> String? in
- let key: String = duration != nil ? "original_purchase_date_ms" : "expires_date_ms"
- return receipt[key] as? String
- }
- .flatMap { (dateString) -> Date? in
- guard let doubleValue = Double(dateString) else { return nil }
- // If duration is set, create an "expires date" value calculated from the original purchase date
- let addedDuration = duration ?? 0
- let expiryDateDouble = (doubleValue / 1000 + addedDuration)
- return Date(timeIntervalSince1970: expiryDateDouble)
- }
- .sorted { (a, b) -> Bool in
- // Sort by descending date order
- return a.compare(b) == .orderedDescending
- }
- guard let firstExpiryDate = expiryDateValues.first else {
- return .notPurchased
- }
- // Check if at least 1 receipt is valid
- if firstExpiryDate.compare(receiptDate) == .orderedDescending {
- // The subscription is valid
- return .purchased(expiryDate: firstExpiryDate)
- } else {
- // The subscription is expired
- return .expired(expiryDate: firstExpiryDate)
- }
- }
- private class func getReceiptsAndDuration(for subscriptionType: SubscriptionType, inReceipt receipt: ReceiptInfo) -> ([ReceiptInfo]?, TimeInterval?) {
- switch subscriptionType {
- case .autoRenewable:
- return (receipt["latest_receipt_info"] as? [ReceiptInfo], nil)
- case .nonRenewing(let duration):
- return (receipt["receipt"]?["in_app"] as? [ReceiptInfo], duration)
- }
- }
- private class func getReceiptRequestDate(inReceipt receipt: ReceiptInfo) -> Date? {
- guard let receiptInfo = receipt["receipt"] as? ReceiptInfo,
- let requestDateString = receiptInfo["request_date_ms"] as? String,
- let requestDateMs = Double(requestDateString) else {
- return nil
- }
- return Date(timeIntervalSince1970: requestDateMs / 1000)
- }
- /**
- * Get all the receipts info for a specific product
- * - Parameter receipts: the receipts array to grab info from
- * - Parameter productId: the product id
- */
- private class func filterReceiptsInfo(receipts: [ReceiptInfo]?, withProductId productId: String) -> [ReceiptInfo] {
- guard let receipts = receipts else {
- return []
- }
- // Filter receipts with matching product id
- let receiptsMatchingProductId = receipts
- .filter { (receipt) -> Bool in
- let product_id = receipt["product_id"] as? String
- return product_id == productId
- }
- return receiptsMatchingProductId
- }
- }
|