InAppReceipt.swift 8.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187
  1. //
  2. // InAppReceipt.swift
  3. // SwiftyStoreKit
  4. //
  5. // Created by phimage on 22/12/15.
  6. // Copyright (c) 2015 Andrea Bizzotto (bizz84@gmail.com)
  7. //
  8. // Permission is hereby granted, free of charge, to any person obtaining a copy
  9. // of this software and associated documentation files (the "Software"), to deal
  10. // in the Software without restriction, including without limitation the rights
  11. // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
  12. // copies of the Software, and to permit persons to whom the Software is
  13. // furnished to do so, subject to the following conditions:
  14. //
  15. // The above copyright notice and this permission notice shall be included in
  16. // all copies or substantial portions of the Software.
  17. //
  18. // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  19. // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  20. // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  21. // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  22. // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  23. // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
  24. // THE SOFTWARE.
  25. import Foundation
  26. // MARK - receipt mangement
  27. internal class InAppReceipt {
  28. static var appStoreReceiptUrl: URL? {
  29. return Bundle.main.appStoreReceiptURL
  30. }
  31. static var appStoreReceiptData: Data? {
  32. guard let receiptDataURL = appStoreReceiptUrl, let data = try? Data(contentsOf: receiptDataURL) else {
  33. return nil
  34. }
  35. return data
  36. }
  37. // The base64 encoded receipt data.
  38. static var appStoreReceiptBase64Encoded: String? {
  39. return appStoreReceiptData?.base64EncodedString(options: [])
  40. }
  41. // https://developer.apple.com/library/ios/releasenotes/General/ValidateAppStoreReceipt/Chapters/ValidateRemotely.html
  42. /**
  43. * - Parameter receiptVerifyURL: receipt verify url (default: Production)
  44. * - Parameter password: Only used for receipts that contain auto-renewable subscriptions. Your app’s shared secret (a hexadecimal string).
  45. * - Parameter session: the session used to make remote call.
  46. * - Parameter completion: handler for result
  47. */
  48. class func verify(using validator: ReceiptValidator,
  49. password autoRenewPassword: String? = nil,
  50. completion: @escaping (VerifyReceiptResult) -> Void) {
  51. // If no receipt is present, validation fails.
  52. guard let base64EncodedString = appStoreReceiptBase64Encoded else {
  53. completion(.error(error: .noReceiptData))
  54. return
  55. }
  56. validator.validate(receipt: base64EncodedString, password: autoRenewPassword, completion: completion)
  57. }
  58. /**
  59. * Verify the purchase of a Consumable or NonConsumable product in a receipt
  60. * - Parameter productId: the product id of the purchase to verify
  61. * - Parameter inReceipt: the receipt to use for looking up the purchase
  62. * - return: either notPurchased or purchased
  63. */
  64. class func verifyPurchase(
  65. productId: String,
  66. inReceipt receipt: ReceiptInfo
  67. ) -> VerifyPurchaseResult {
  68. // Get receipts info for the product
  69. let receipts = receipt["receipt"]?["in_app"] as? [ReceiptInfo]
  70. let receiptsInfo = filterReceiptsInfo(receipts: receipts, withProductId: productId)
  71. // Verify that at least one receipt has the right product id
  72. return receiptsInfo.count >= 1 ? .purchased : .notPurchased
  73. }
  74. /**
  75. * 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.
  76. * - parameter type: .autoRenewable or .nonRenewing(duration)
  77. * - Parameter productId: the product id of the purchase to verify
  78. * - Parameter inReceipt: the receipt to use for looking up the subscription
  79. * - Parameter validUntil: date to check against the expiry date of the subscription. If nil, no verification
  80. * - Parameter validDuration: the duration of the subscription. Only required for non-renewable subscription.
  81. * - return: either NotPurchased or Purchased / Expired with the expiry date found in the receipt
  82. */
  83. class func verifySubscription(
  84. type: SubscriptionType,
  85. productId: String,
  86. inReceipt receipt: ReceiptInfo,
  87. validUntil date: Date = Date()
  88. ) -> VerifySubscriptionResult {
  89. // Verify that at least one receipt has the right product id
  90. // 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.
  91. let (receipts, duration) = getReceiptsAndDuration(for: type, inReceipt: receipt)
  92. let receiptsInfo = filterReceiptsInfo(receipts: receipts, withProductId: productId)
  93. let nonCancelledReceiptsInfo = receiptsInfo.filter { receipt in receipt["cancellation_date"] == nil }
  94. if nonCancelledReceiptsInfo.count == 0 {
  95. return .notPurchased
  96. }
  97. let receiptDate = getReceiptRequestDate(inReceipt: receipt) ?? date
  98. // Return the expires dates sorted desc
  99. let expiryDateValues = nonCancelledReceiptsInfo
  100. .flatMap { (receipt) -> String? in
  101. let key: String = duration != nil ? "original_purchase_date_ms" : "expires_date_ms"
  102. return receipt[key] as? String
  103. }
  104. .flatMap { (dateString) -> Date? in
  105. guard let doubleValue = Double(dateString) else { return nil }
  106. // If duration is set, create an "expires date" value calculated from the original purchase date
  107. let addedDuration = duration ?? 0
  108. let expiryDateDouble = (doubleValue / 1000 + addedDuration)
  109. return Date(timeIntervalSince1970: expiryDateDouble)
  110. }
  111. .sorted { (a, b) -> Bool in
  112. // Sort by descending date order
  113. return a.compare(b) == .orderedDescending
  114. }
  115. guard let firstExpiryDate = expiryDateValues.first else {
  116. return .notPurchased
  117. }
  118. // Check if at least 1 receipt is valid
  119. if firstExpiryDate.compare(receiptDate) == .orderedDescending {
  120. // The subscription is valid
  121. return .purchased(expiryDate: firstExpiryDate)
  122. } else {
  123. // The subscription is expired
  124. return .expired(expiryDate: firstExpiryDate)
  125. }
  126. }
  127. private class func getReceiptsAndDuration(for subscriptionType: SubscriptionType, inReceipt receipt: ReceiptInfo) -> ([ReceiptInfo]?, TimeInterval?) {
  128. switch subscriptionType {
  129. case .autoRenewable:
  130. return (receipt["latest_receipt_info"] as? [ReceiptInfo], nil)
  131. case .nonRenewing(let duration):
  132. return (receipt["receipt"]?["in_app"] as? [ReceiptInfo], duration)
  133. }
  134. }
  135. private class func getReceiptRequestDate(inReceipt receipt: ReceiptInfo) -> Date? {
  136. guard let receiptInfo = receipt["receipt"] as? ReceiptInfo,
  137. let requestDateString = receiptInfo["request_date_ms"] as? String,
  138. let requestDateMs = Double(requestDateString) else {
  139. return nil
  140. }
  141. return Date(timeIntervalSince1970: requestDateMs / 1000)
  142. }
  143. /**
  144. * Get all the receipts info for a specific product
  145. * - Parameter receipts: the receipts array to grab info from
  146. * - Parameter productId: the product id
  147. */
  148. private class func filterReceiptsInfo(receipts: [ReceiptInfo]?, withProductId productId: String) -> [ReceiptInfo] {
  149. guard let receipts = receipts else {
  150. return []
  151. }
  152. // Filter receipts with matching product id
  153. let receiptsMatchingProductId = receipts
  154. .filter { (receipt) -> Bool in
  155. let product_id = receipt["product_id"] as? String
  156. return product_id == productId
  157. }
  158. return receiptsMatchingProductId
  159. }
  160. }