InAppReceipt.swift 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286
  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. extension Date {
  27. init?(millisecondsSince1970: String) {
  28. guard let millisecondsNumber = Double(millisecondsSince1970) else {
  29. return nil
  30. }
  31. self = Date(timeIntervalSince1970: millisecondsNumber / 1000)
  32. }
  33. }
  34. extension ReceiptItem {
  35. public init?(receiptInfo: ReceiptInfo) {
  36. guard
  37. let productId = receiptInfo["product_id"] as? String,
  38. let quantityString = receiptInfo["quantity"] as? String,
  39. let quantity = Int(quantityString),
  40. let transactionId = receiptInfo["transaction_id"] as? String,
  41. let originalTransactionId = receiptInfo["original_transaction_id"] as? String,
  42. let purchaseDate = ReceiptItem.parseDate(from: receiptInfo, key: "purchase_date_ms"),
  43. let originalPurchaseDate = ReceiptItem.parseDate(from: receiptInfo, key: "original_purchase_date_ms")
  44. else {
  45. print("could not parse receipt item: \(receiptInfo). Skipping...")
  46. return nil
  47. }
  48. self.productId = productId
  49. self.quantity = quantity
  50. self.transactionId = transactionId
  51. self.originalTransactionId = originalTransactionId
  52. self.purchaseDate = purchaseDate
  53. self.originalPurchaseDate = originalPurchaseDate
  54. self.webOrderLineItemId = receiptInfo["web_order_line_item_id"] as? String
  55. self.subscriptionExpirationDate = ReceiptItem.parseDate(from: receiptInfo, key: "expires_date_ms")
  56. self.cancellationDate = ReceiptItem.parseDate(from: receiptInfo, key: "cancellation_date_ms")
  57. if let isTrialPeriod = receiptInfo["is_trial_period"] as? String {
  58. self.isTrialPeriod = Bool(isTrialPeriod) ?? false
  59. } else {
  60. self.isTrialPeriod = false
  61. }
  62. if let isInIntroOfferPeriod = receiptInfo["is_in_intro_offer_period"] as? String {
  63. self.isInIntroOfferPeriod = Bool(isInIntroOfferPeriod) ?? false
  64. } else {
  65. self.isInIntroOfferPeriod = false
  66. }
  67. self.isUpgraded = receiptInfo["is_upgraded"] as? Bool ?? false
  68. }
  69. private static func parseDate(from receiptInfo: ReceiptInfo, key: String) -> Date? {
  70. guard
  71. let requestDateString = receiptInfo[key] as? String,
  72. let requestDateMs = Double(requestDateString) else {
  73. return nil
  74. }
  75. return Date(timeIntervalSince1970: requestDateMs / 1000)
  76. }
  77. }
  78. // MARK: - receipt mangement
  79. internal class InAppReceipt {
  80. /**
  81. * Verify the purchase of a Consumable or NonConsumable product in a receipt
  82. * - Parameter productId: the product id of the purchase to verify
  83. * - Parameter inReceipt: the receipt to use for looking up the purchase
  84. * - return: either notPurchased or purchased
  85. */
  86. class func verifyPurchase(
  87. productId: String,
  88. inReceipt receipt: ReceiptInfo
  89. ) -> VerifyPurchaseResult {
  90. // Get receipts info for the product
  91. let receipts = getInAppReceipts(receipt: receipt)
  92. let filteredReceiptsInfo = filterReceiptsInfo(receipts: receipts, withProductIds: [productId])
  93. let nonCancelledReceiptsInfo = filteredReceiptsInfo.filter { receipt in receipt["cancellation_date"] == nil }
  94. #if swift(>=4.1)
  95. let receiptItems = nonCancelledReceiptsInfo.compactMap { ReceiptItem(receiptInfo: $0) }
  96. #else
  97. let receiptItems = nonCancelledReceiptsInfo.flatMap { ReceiptItem(receiptInfo: $0) }
  98. #endif
  99. // Verify that at least one receipt has the right product id
  100. if let firstItem = receiptItems.first {
  101. return .purchased(item: firstItem)
  102. }
  103. return .notPurchased
  104. }
  105. /**
  106. * Verify the validity of a set of subscriptions in a receipt.
  107. *
  108. * This method extracts all transactions matching the given productIds and sorts them by date in descending order. It then compares the first transaction expiry date against the receipt date, to determine its validity.
  109. * - Note: You can use this method to check the validity of (mutually exclusive) subscriptions in a subscription group.
  110. * - Remark: The type parameter determines how the expiration dates are calculated for all subscriptions. Make sure all productIds match the specified subscription type to avoid incorrect results.
  111. * - Parameter type: .autoRenewable or .nonRenewing.
  112. * - Parameter productIds: The product ids of the subscriptions to verify.
  113. * - Parameter receipt: The receipt to use for looking up the subscriptions
  114. * - Parameter validUntil: Date to check against the expiry date of the subscriptions. This is only used if a date is not found in the receipt.
  115. * - return: Either .notPurchased or .purchased / .expired with the expiry date found in the receipt.
  116. */
  117. class func verifySubscriptions(
  118. ofType type: SubscriptionType,
  119. productIds: Set<String>,
  120. inReceipt receipt: ReceiptInfo,
  121. validUntil date: Date = Date()
  122. ) -> VerifySubscriptionResult {
  123. // 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.
  124. let (receipts, duration) = getReceiptsAndDuration(for: type, inReceipt: receipt)
  125. let receiptsInfo = filterReceiptsInfo(receipts: receipts, withProductIds: productIds)
  126. let nonCancelledReceiptsInfo = receiptsInfo.filter { receipt in receipt["cancellation_date"] == nil }
  127. if nonCancelledReceiptsInfo.count == 0 {
  128. return .notPurchased
  129. }
  130. let receiptDate = getReceiptRequestDate(inReceipt: receipt) ?? date
  131. #if swift(>=4.1)
  132. let receiptItems = nonCancelledReceiptsInfo.compactMap { ReceiptItem(receiptInfo: $0) }
  133. #else
  134. let receiptItems = nonCancelledReceiptsInfo.flatMap { ReceiptItem(receiptInfo: $0) }
  135. #endif
  136. if nonCancelledReceiptsInfo.count > receiptItems.count {
  137. print("receipt has \(nonCancelledReceiptsInfo.count) items, but only \(receiptItems.count) were parsed")
  138. }
  139. let sortedExpiryDatesAndItems = expiryDatesAndItems(receiptItems: receiptItems, duration: duration).sorted { a, b in
  140. return a.0 > b.0
  141. }
  142. guard let firstExpiryDateItemPair = sortedExpiryDatesAndItems.first else {
  143. return .notPurchased
  144. }
  145. let sortedReceiptItems = sortedExpiryDatesAndItems.map { $0.1 }
  146. if firstExpiryDateItemPair.0 > receiptDate {
  147. return .purchased(expiryDate: firstExpiryDateItemPair.0, items: sortedReceiptItems)
  148. } else {
  149. return .expired(expiryDate: firstExpiryDateItemPair.0, items: sortedReceiptItems)
  150. }
  151. }
  152. /**
  153. * Get the distinct product identifiers from receipt.
  154. *
  155. * This Method extracts all product identifiers. (Including cancelled ones).
  156. * - Note: You can use this method to get all unique product identifiers from receipt.
  157. * - Parameter type: .autoRenewable or .nonRenewing.
  158. * - Parameter receipt: The receipt to use for looking up the product identifiers.
  159. * - return: Either Set<String> or nil.
  160. */
  161. class func getDistinctPurchaseIds(
  162. ofType type: SubscriptionType,
  163. inReceipt receipt: ReceiptInfo
  164. ) -> Set<String>? {
  165. // Get receipts array from receipt
  166. guard let receipts = getReceipts(for: type, inReceipt: receipt) else {
  167. return nil
  168. }
  169. #if swift(>=4.1)
  170. let receiptIds = receipts.compactMap { ReceiptItem(receiptInfo: $0)?.productId }
  171. #else
  172. let receiptIds = receipts.flatMap { ReceiptItem(receiptInfo: $0)?.productId }
  173. #endif
  174. if receiptIds.isEmpty {
  175. return nil
  176. }
  177. return Set(receiptIds)
  178. }
  179. private class func expiryDatesAndItems(receiptItems: [ReceiptItem], duration: TimeInterval?) -> [(Date, ReceiptItem)] {
  180. if let duration = duration {
  181. return receiptItems.map {
  182. let expirationDate = Date(timeIntervalSince1970: $0.originalPurchaseDate.timeIntervalSince1970 + duration)
  183. return (expirationDate, $0)
  184. }
  185. } else {
  186. #if swift(>=4.1)
  187. return receiptItems.compactMap {
  188. if let expirationDate = $0.subscriptionExpirationDate {
  189. return (expirationDate, $0)
  190. }
  191. return nil
  192. }
  193. #else
  194. return receiptItems.flatMap {
  195. if let expirationDate = $0.subscriptionExpirationDate {
  196. return (expirationDate, $0)
  197. }
  198. return nil
  199. }
  200. #endif
  201. }
  202. }
  203. private class func getReceipts(for subscriptionType: SubscriptionType, inReceipt receipt: ReceiptInfo) -> [ReceiptInfo]? {
  204. switch subscriptionType {
  205. case .autoRenewable:
  206. return receipt["latest_receipt_info"] as? [ReceiptInfo]
  207. case .nonRenewing:
  208. return getInAppReceipts(receipt: receipt)
  209. }
  210. }
  211. private class func getReceiptsAndDuration(for subscriptionType: SubscriptionType, inReceipt receipt: ReceiptInfo) -> ([ReceiptInfo]?, TimeInterval?) {
  212. switch subscriptionType {
  213. case .autoRenewable:
  214. return (receipt["latest_receipt_info"] as? [ReceiptInfo], nil)
  215. case .nonRenewing(let duration):
  216. return (getInAppReceipts(receipt: receipt), duration)
  217. }
  218. }
  219. private class func getReceiptRequestDate(inReceipt receipt: ReceiptInfo) -> Date? {
  220. guard let receiptInfo = receipt["receipt"] as? ReceiptInfo,
  221. let requestDateString = receiptInfo["request_date_ms"] as? String else {
  222. return nil
  223. }
  224. return Date(millisecondsSince1970: requestDateString)
  225. }
  226. private class func getInAppReceipts(receipt: ReceiptInfo) -> [ReceiptInfo]? {
  227. let appReceipt = receipt["receipt"] as? ReceiptInfo
  228. return appReceipt?["in_app"] as? [ReceiptInfo]
  229. }
  230. /**
  231. * Get all the receipts info for a specific product
  232. * - Parameter receipts: the receipts array to grab info from
  233. * - Parameter productId: the product id
  234. */
  235. private class func filterReceiptsInfo(receipts: [ReceiptInfo]?, withProductIds productIds: Set<String>) -> [ReceiptInfo] {
  236. guard let receipts = receipts else {
  237. return []
  238. }
  239. // Filter receipts with matching product ids
  240. let receiptsMatchingProductIds = receipts
  241. .filter { (receipt) -> Bool in
  242. if let productId = receipt["product_id"] as? String {
  243. return productIds.contains(productId)
  244. }
  245. return false
  246. }
  247. return receiptsMatchingProductIds
  248. }
  249. }