Merge branch 'master' into develop

This commit is contained in:
Pavel Tikhonenko
2021-05-28 21:42:25 +03:00
20 changed files with 927 additions and 271 deletions
+18
View File
@@ -0,0 +1,18 @@
name: Swift
on:
push:
branches: [ master ]
pull_request:
branches: [ master ]
jobs:
build:
runs-on: macos-latest
steps:
- uses: actions/checkout@v2
- name: Build
run: swift build -v
+44
View File
@@ -0,0 +1,44 @@
# Use TPInAppReceipt in Objective-C project
Installation
------------
### CocoaPods
To integrate TPInAppReceipt into your Objective-C project using CocoaPods, specify it in your `Podfile`:
```ruby
platform :ios, '9.0'
target 'YOUR_TARGET' do
use_frameworks!
pod 'TPInAppReceipt/Objc'
end
```
Then, run the following command:
```bash
$ pod install
```
In any swift file you'd like to use TPInAppReceipt, import the framework with `@import TPInAppReceipt;`.
### Swift Package Manager
To integrate using Apple's Swift package manager, add the following as a dependency to your `Package.swift`:
```swift
.package(url: "https://github.com/tikhop/TPInAppReceipt.git", .upToNextMajor(from: "3.0.0"))
```
Then, specify `"TPInAppReceipt-Objc"` as a dependency of the Target in which you wish to use TPInAppReceipt.
Lastly, run the following command:
```swift
swift package update
```
Once you are done with SPM, you can import the framework with `@import TPInAppReceipt_Objc;`.
+9 -2
View File
@@ -11,6 +11,7 @@ let package = Package(
products: [
.library(name: "TPInAppReceipt", targets: ["TPInAppReceipt"]),
.library(name: "TPInAppReceipt-Objc", targets: ["TPInAppReceipt-Objc"]),
],
dependencies: [
.package(url: "https://github.com/tikhop/ASN1Swift", .upToNextMajor(from: "1.0.0"))
@@ -20,13 +21,19 @@ let package = Package(
name: "TPInAppReceipt",
dependencies: ["ASN1Swift"],
path: "Sources",
exclude: ["Bundle+Extension.swift"],
exclude: ["Bundle+Extension.swift", "Objc/InAppReceipt+Objc.swift"],
resources: [.process("AppleIncRootCertificate.cer"), .process("StoreKitTestCertificate.cer")]
),
.target(
name: "TPInAppReceipt-Objc",
dependencies: ["TPInAppReceipt"],
path: "Sources/Objc"
),
.testTarget(
name: "TPInAppReceiptTests",
dependencies: ["TPInAppReceipt"])
]
],
swiftLanguageVersions: [.v5]
)
+73 -62
View File
@@ -5,25 +5,27 @@
# TPInAppReceipt
[![Carthage compatible](https://img.shields.io/badge/Carthage-compatible-4BC51D.svg?style=flat)](https://github.com/Carthage/Carthage)
![Swift](https://github.com/tikhop/TPInAppReceipt/workflows/Swift/badge.svg?branch=master)
[![CocoaPods Compatible](https://img.shields.io/cocoapods/v/TPInAppReceipt.svg)](https://cocoapods.org/pods/TPInAppReceipt)
[![Swift Package Manager compatible](https://img.shields.io/badge/Swift%20Package%20Manager-compatible-brightgreen.svg)](https://github.com/apple/swift-package-manager)
[![Platform](https://img.shields.io/cocoapods/p/TPInAppReceipt.svg?style=flat)]()
[![GitHub license](https://img.shields.io/badge/license-MIT-blue.svg)](https://raw.githubusercontent.com/tikhop/TPInAppReceipt/master/LICENSE)
A lightweight library for reading and validating Apple In App Purchase Receipt locally.
TPInAppReceipt is a lightweight, pure-Swift library for reading and validating Apple In App Purchase Receipt locally.
## Features
- [x] Extract all In-App Receipt Attributes
- [x] Hash Verification
- [x] Verify Bundle Version and Identifiers
- [x] Signature Verification
- [x] Read all In-App Receipt Attributes
- [x] Validate In-App Purchase Receipt (Signature, Bundle Version and Identifier, Hash)
- [x] Determine Eligibility for Introductory Offer
- [x] Use with StoreKitTest
- [x] Use in Objective-C projects
Installation
------------
> Note: [TPInAppReceipt in Objective-C project](https://github.com/tikhop/TPInAppReceipt/blob/master/Documentation/UseInObjCProject.md) - If you want to use TPInAppReceipt in Objective-C project please follow this guide.
### CocoaPods
To integrate TPInAppReceipt into your project using CocoaPods, specify it in your `Podfile`:
@@ -32,9 +34,9 @@ To integrate TPInAppReceipt into your project using CocoaPods, specify it in you
platform :ios, '9.0'
target 'YOUR_TARGET' do
use_frameworks!
use_frameworks!
pod 'TPInAppReceipt'
pod 'TPInAppReceipt'
end
```
@@ -62,22 +64,9 @@ Lastly, run the following command:
swift package update
```
### Carthage
Make the following entry in your Cartfile:
```
github "tikhop/TPInAppReceipt"
```
Then run `carthage update`.
If this is your first time using Carthage in the project, you'll need to go through some additional steps as explained [over at Carthage](https://github.com/Carthage/Carthage#adding-frameworks-to-an-application).
### Requirements
- iOS 9.0+ / OSX 10.11+
- iOS 10.0+ / OSX 10.11+
- Swift 5.3+
Usage
@@ -85,14 +74,17 @@ Usage
### Working With a Receipt
`InAppReceipt` is an object to incapsulate all necessary getters from a receipt payload and provides a comprehensive API for reading and validating in app receipt and related purchases.
The [`InAppReceipt`](https://tikhop.github.io/TPInAppReceipt/Classes/InAppReceipt.html) object encapsulates information about a receipt and the purchases associated with it. To validate In-App Purchase Receipt you must create an `InAppReceipt` object.
#### Initializing Receipt
To create [`InAppReceipt`](https://tikhop.github.io/TPInAppReceipt/Classes/InAppReceipt.html) object you can either provide a raw receipt data or initialize a local receipt.
```swift
do {
/// Initialize receipt
let receipt = try InAppReceipt.localReceipt()
// let receipt = try InAppReceipt() // Returns local receipt
// let receiptData: Data = ...
// let receipt = try InAppReceipt.receipt(from: receiptData)
@@ -104,31 +96,64 @@ do {
```
#### Refreshing/Requesting Receipt
#### Validating Receipt
Use this method to request a new receipt if the receipt is invalid or missing.
`TPInAppReceipt` provides a variety of convenience methods for validating In-App Purchase Receipt:
```swift
InAppReceipt.refresh { (error) in
if let err = error
{
print(err)
}else{
initializeReceipt()
}
/// Verify hash
try? receipt.verifyHash()
/// Verify bundle identifier
try? receipt.verifyBundleIdentifier()
/// Verify bundle version
try? receipt.verifyBundleVersion()
/// Verify signature
try? receipt.verifySignature()
/// Validate all at once
do {
try receipt.verify()
} catch IARError.validationFailed(reason: .hashValidation) {
// Do smth
} catch IARError.validationFailed(reason: .bundleIdentifierVerification) {
// Do smth
} catch IARError.validationFailed(reason: .signatureValidation) {
// Do smth
} catch {
// Do smth
}
```
> NOTE: Apple recommends to perform receipt validation right after your app is launched. For additional security, you may repeat this check periodically while your application is running.
> NOTE: If validation fails in iOS, try to refresh the receipt first.
#### Determining Eligibility for Introductory Offer
If your App offers introductory pricing for auto-renewable subscriptions, you will need to dispay the correct price, either the intro or regular price.
The [`InAppReceipt`](https://tikhop.github.io/TPInAppReceipt/Classes/InAppReceipt.html) class provides an interface for determining introductory price eligibility. At the simplest, just provide a `Set` of product identifiers that belong to the same subscription group:
```swift
// Check whether user is eligible for any products within the same subscription group
var isEligible = receipt.isEligibleForIntroductoryOffer(for: ["com.test.product.bronze", "com.test.product.silver", "com.test.product.gold"])
```
> Note: To determine if a user is eligible for an introductory offer, you must initialize and validate receipt first and only then check for eligibility.
#### Reading Receipt
```swift
/// Base64 Encoded Receipt
let base64Receipt = receipt.base64
/// Initialize receipt
let receipt = try! InAppReceipt.localReceipt()
/// Base64 Encoded Receipt
let base64Receipt = receipt.base64
/// Check whether receipt contains any purchases
let hasPurchases = receipt.hasPurchases
@@ -155,44 +180,30 @@ receipt.purchases(ofProductIdentifier: subscriptionName)
```
#### Validating Receipt
#### Refreshing/Requesting Receipt
When necessary, use this method to ensure the receipt you are working with is up-to-date.
```swift
/// Verify all at once
do {
try r.verify()
} catch IARError.validationFailed(reason: .hashValidation)
{
// Do smth
} catch IARError.validationFailed(reason: .bundleIdentifierVerification)
{
// Do smth
} catch IARError.validationFailed(reason: .signatureValidation)
{
// Do smth
} catch {
// Do smth
InAppReceipt.refresh { (error) in
if let err = error
{
print(err)
} else {
initializeReceipt()
}
}
/// Verify hash
try? r.verifyHash()
/// Verify bundle identifier and version
try? r.verifyBundleIdentifierAndVersion()
/// Verify signature
try? r.verifySignature()
```
## Essential Reading
* [Apple - About Receipt Validation](https://developer.apple.com/library/content/releasenotes/General/ValidateAppStoreReceipt/Introduction.html)
* [Apple - Receipt Validation Programming Guide](https://developer.apple.com/library/content/releasenotes/General/ValidateAppStoreReceipt/Chapters/ReceiptFields.html#//apple_ref/doc/uid/TP40010573-CH106-SW1)
* [Apple - Validating Receipts Locally](https://developer.apple.com/library/content/releasenotes/General/ValidateAppStoreReceipt/Chapters/ValidateLocally.html)
* [fluffy.es - Tutorial: Read and validate in-app purchase receipt locally using TPInAppReceipt](https://fluffy.es/in-app-purchase-receipt-local/)
* [objc.io - Receipt Validation](https://www.objc.io/issues/17-security/receipt-validation/)
## License
TPInAppReceipt is released under an MIT license. See [LICENSE](https://github.com/tikhop/TPInAppReceipt/blob/master/LICENSE) for more information.
+1 -1
View File
@@ -3,7 +3,7 @@
// TPInAppReceipt
//
// Created by Pavel Tikhonenko on 26.06.2020.
// Copyright © 2020 Pavel Tikhonenko. All rights reserved.
// Copyright © 2020-2021 Pavel Tikhonenko. All rights reserved.
//
import class Foundation.Bundle
+1 -1
View File
@@ -3,7 +3,7 @@
// TPReceiptValidator
//
// Created by Pavel Tikhonenko on 29/09/16.
// Copyright © 2016-2020 Pavel Tikhonenko. All rights reserved.
// Copyright © 2016-2021 Pavel Tikhonenko. All rights reserved.
//
import Foundation
+9 -17
View File
@@ -3,7 +3,7 @@
// TPInAppReceipt
//
// Created by Pavel Tikhonenko on 01/10/16.
// Copyright © 2016-2020 Pavel Tikhonenko. All rights reserved.
// Copyright © 2016-2021 Pavel Tikhonenko. All rights reserved.
//
import Foundation
@@ -18,24 +18,16 @@ public extension Date
public extension String
{
func utcTime() -> Date?
{
let formatter = ISO8601DateFormatter()
formatter.timeZone = TimeZone(abbreviation: "UTC")
formatter.formatOptions = .withInternetDateTime
let date = formatter.date(from: self)
return date
}
func rfc3339date() -> Date?
{
let formatter = ISO8601DateFormatter()
formatter.formatOptions = .withInternetDateTime
formatter.timeZone = TimeZone(abbreviation: "UTC")
let date = formatter.date(from: self)
let date = rfc3339DateFormatter.date(from: self)
return date
}
}
fileprivate var rfc3339DateFormatter: ISO8601DateFormatter = {
let formatter = ISO8601DateFormatter()
formatter.formatOptions = .withInternetDateTime
formatter.timeZone = TimeZone(abbreviation: "UTC")
return formatter
}()
+160 -6
View File
@@ -1,9 +1,9 @@
//
// InAppReceiptManager.swift
// Extras.swift
// TPInAppReceipt
//
// Created by Pavel Tikhonenko on 13.02.2020.
// Copyright © 2020 Pavel Tikhonenko. All rights reserved.
// Copyright © 2021 Pavel Tikhonenko. All rights reserved.
//
#if canImport(StoreKit)
@@ -14,12 +14,127 @@ import StoreKit
@available(watchOSApplicationExtension 6.2, *)
fileprivate var refreshSession: RefreshSession?
@available(tvOS 12.0, *)
@available(macOS 10.14, *)
@available(iOS 12.0, *)
public class SKSubscriptionGroup
{
let identifier: GroupIdentifier
var products: Set<SKProduct> = []
init(with identifier: GroupIdentifier)
{
self.identifier = identifier
self.products = []
}
init(with products: Set<SKProduct>)
{
guard let gid = products.first?.subscriptionGroupIdentifier else
{
fatalError("All products must have subscriptionGroupIdentifier")
}
self.identifier = gid
self.products = products
}
func insert(product: SKProduct)
{
if identifier != product.subscriptionGroupIdentifier
{
fatalError("`Product.subscriptionGroupIdentifier` must be equal to current identifier")
}
products.insert(product)
}
func contains(_ productIdentifier: String) -> Bool
{
return products.contains(where: { $0.productIdentifier == productIdentifier })
}
}
public typealias GroupIdentifier = String
@available(tvOS 12.0, *)
@available(iOS 12.0, *)
@available(macOS 10.14, *)
public extension SKProductsResponse
{
/// Build a `SKSubscriptionGroup` object
///
/// We assume that all retrieved products `(SKProduct)` belong to the same subscription group
///
/// - Returns `SKSubscriptionGroup`. Empty if no subscription groups found
var subscriptionGroup: SKSubscriptionGroup
{
var group: SKSubscriptionGroup!
for p in products
{
guard let pgid = p.subscriptionGroupIdentifier else
{
continue
}
if group == nil
{
group = SKSubscriptionGroup(with: pgid)
}
group.insert(product: p)
}
guard let g = group else
{
fatalError("`group` can't be nil.")
}
return g
}
/// Build a dictionary that contains the subscription groups
///
/// The dictionary contains `SKSubscriptionGroup` objects where the key is a group identifier `GroupIdentifier`
///
/// - Returns `[GroupIdentifier: SKSubscriptionGroup]`. Empty if no subscription groups found
var subscriptionGroups: [GroupIdentifier: SKSubscriptionGroup]
{
var groups: [GroupIdentifier: SKSubscriptionGroup] = [:]
for p in products
{
guard let gid = p.subscriptionGroupIdentifier else
{
continue
}
guard let group = groups[gid] else
{
let g = SKSubscriptionGroup(with: gid)
g.insert(product: p)
groups[gid] = g
continue
}
group.insert(product: p)
}
return groups
}
}
@available(tvOS 12.0, *)
@available(macOS 10.14, *)
public extension InAppReceipt
{
/**
* Refresh local in-app receipt
* - Parameter completion: handler for result
*/
/// Refresh local in-app receipt
///
/// - Parameter completion: handler for result
@available(watchOSApplicationExtension 6.2, *)
static func refresh(completion: @escaping IAPRefreshRequestResult)
{
@@ -37,6 +152,18 @@ public extension InAppReceipt
{
refreshSession = nil
}
/// Check whether user is eligible for introductory offer for any products within the same subscription group
///
/// - Returns `false` if user isn't eligible for introductory offer, otherwise `true`
@available(iOS 12.0, *)
func isEligibleForIntroductoryOffer(for group: SKSubscriptionGroup) -> Bool
{
let array = purchases.filter { $0.subscriptionTrialPeriod || $0.subscriptionIntroductoryPricePeriod }
.filter { group.contains($0.productIdentifier) }
return array.isEmpty
}
}
public typealias IAPRefreshRequestResult = ((Error?) -> ())
@@ -85,3 +212,30 @@ fileprivate class RefreshSession : NSObject, SKRequestDelegate
}
#endif
public typealias SubscriptionGroup = Set<String>
public extension InAppReceipt
{
/// Check whether user is eligible for introductory offer for any products within the same subscription group
///
/// - Returns `false` if user isn't eligible for introductory offer, otherwise `true`
func isEligibleForIntroductoryOffer(for group: SubscriptionGroup) -> Bool
{
let array = purchases.filter { $0.subscriptionTrialPeriod || $0.subscriptionIntroductoryPricePeriod }
.filter { group.contains($0.productIdentifier) }
return array.isEmpty
}
/// Check whether user is eligible for introductory offer for a specific product
///
/// - Returns `false` if user isn't eligible for introductory offer, otherwise `true`
func isEligibleForIntroductoryOffer(for productIdentifier: String) -> Bool
{
let array = purchases.filter { $0.subscriptionTrialPeriod || $0.subscriptionIntroductoryPricePeriod }
.filter { $0.productIdentifier == productIdentifier }
return array.isEmpty
}
}
+2 -2
View File
@@ -1,9 +1,9 @@
//
// Constants.swift
// IARError.swift
// TPInAppReceipt
//
// Created by Pavel Tikhonenko on 20/01/17.
// Copyright © 2017-2020 Pavel Tikhonenko. All rights reserved.
// Copyright © 2017-2021 Pavel Tikhonenko. All rights reserved.
//
import Foundation
+14 -37
View File
@@ -3,7 +3,7 @@
// TPInAppReceipt
//
// Created by Pavel Tikhonenko on 19/01/17.
// Copyright © 2017-2020 Pavel Tikhonenko. All rights reserved.
// Copyright © 2017-2021 Pavel Tikhonenko. All rights reserved.
//
import Foundation
@@ -40,17 +40,17 @@ public struct InAppPurchase
/// Original Transaction identifier
public var originalTransactionIdentifier: String
/// Purchase Date in string format
public var purchaseDateString: String
/// Purchase Date
public var purchaseDate: Date
/// Original Purchase Date in string format
public var originalPurchaseDateString: String
/// Original Purchase Date. Returns `nil` when testing with StoreKitTest
public var originalPurchaseDate: Date! = nil
/// Subscription Expiration Date in string format. Returns `nil` if the purchase is not a renewable subscription
public var subscriptionExpirationDateString: String? = nil
/// Subscription Expiration Date. Returns `nil` if the purchase has been expired (in some cases)
public var subscriptionExpirationDate: Date? = nil
/// Cancellation Date in string format. Returns `nil` if the purchase is not a renewable subscription
public var cancellationDateString: String? = nil
/// Cancellation Date. Returns `nil` if the purchase is not a renewable subscription
public var cancellationDate: Date? = nil
/// This value is `true`if the customers subscription is currently in the free trial period, or `false` if not.
public var subscriptionTrialPeriod: Bool = false
@@ -68,37 +68,14 @@ public struct InAppPurchase
/// The number of consumable products purchased
/// The default value is `1` unless modified with a mutable payment. The maximum value is 10.
public var quantity: Int = 1
public init()
{
originalTransactionIdentifier = ""
productIdentifier = ""
transactionIdentifier = ""
purchaseDateString = ""
originalPurchaseDateString = ""
}
}
public extension InAppPurchase
{
/// Purchase Date representation as a 'Date' object
var purchaseDate: Date
{
return purchaseDateString.rfc3339date()!
}
/// Subscription Expiration Date representation as a 'Date' object. Returns `nil` if the purchase has been expired (in some cases)
var subscriptionExpirationDate: Date?
{
assert(isRenewableSubscription, "\(productIdentifier) is not an auto-renewable subscription.")
return subscriptionExpirationDateString?.rfc3339date()
}
/// A Boolean value indicating whether the purchase is renewable subscription.
var isRenewableSubscription: Bool
{
return self.subscriptionExpirationDateString != nil
return subscriptionExpirationDate != nil
}
/// Check whether the subscription is active for a specific date
@@ -109,10 +86,10 @@ public extension InAppPurchase
{
assert(isRenewableSubscription, "\(productIdentifier) is not an auto-renewable subscription.")
if(self.cancellationDateString != nil && self.cancellationDateString != "")
{
return false
}
if cancellationDate != nil
{
return false
}
guard let expirationDate = subscriptionExpirationDate else
{
+32 -13
View File
@@ -3,7 +3,7 @@
// TPInAppReceipt
//
// Created by Pavel Tikhonenko on 04/08/20.
// Copyright © 2017-2020 Pavel Tikhonenko. All rights reserved.
// Copyright © 2017-2021 Pavel Tikhonenko. All rights reserved.
//
import Foundation
@@ -108,8 +108,9 @@ extension InAppReceiptPayload: ASN1Decodable
var purchases = [InAppPurchase]()
var opaqueValue = Data()
var receiptHash = Data()
var expirationDate: String? = ""
var receiptCreationDate: String = ""
var expirationDate: Date?
var receiptCreationDate: Date!
var ageRating: String = ""
var environment: String = ""
let c = try decoder.container(keyedBy: CodingKeys.self)
@@ -140,9 +141,13 @@ extension InAppReceiptPayload: ASN1Decodable
case InAppReceiptField.originalAppVersion:
originalAppVersion = try valueContainer.decode(String.self)
case InAppReceiptField.expirationDate:
expirationDate = try valueContainer.decode(String.self, template: .universal(ASN1Identifier.Tag.ia5String))
let expirationDateString = try valueContainer.decode(String.self, template: .universal(ASN1Identifier.Tag.ia5String))
expirationDate = expirationDateString.rfc3339date()
case InAppReceiptField.receiptCreationDate:
receiptCreationDate = try valueContainer.decode(String.self, template: .universal(ASN1Identifier.Tag.ia5String))
let receiptCreationDateString = try valueContainer.decode(String.self, template: .universal(ASN1Identifier.Tag.ia5String))
receiptCreationDate = receiptCreationDateString.rfc3339date()
case InAppReceiptField.ageRating:
ageRating = try valueContainer.decode(String.self, template: .universal(ASN1Identifier.Tag.ia5String))
case InAppReceiptField.environment:
environment = try valueContainer.decode(String.self)
default:
@@ -162,6 +167,7 @@ extension InAppReceiptPayload: ASN1Decodable
opaqueValue: opaqueValue,
receiptHash: receiptHash,
creationDate: receiptCreationDate,
ageRating: ageRating,
environment: environment,
rawData: rawData)
}
@@ -171,10 +177,14 @@ extension InAppPurchase: ASN1Decodable
{
public init(from decoder: Decoder) throws
{
self.init()
var container = try decoder.unkeyedContainer() as! ASN1UnkeyedDecodingContainerProtocol
var originalTransactionIdentifier = ""
var productIdentifier = ""
var transactionIdentifier = ""
var purchaseDate: Date!
var originalPurchaseDate: Date!
while !container.isAtEnd
{
do
@@ -183,8 +193,7 @@ extension InAppPurchase: ASN1Decodable
let type: Int32 = try attributeContainer.decode(Int32.self)
let _ = try attributeContainer.skip(template: .universal(ASN1Identifier.Tag.integer)) // Consume
var valueContainer = try attributeContainer.nestedUnkeyedContainer(for: .universal(ASN1Identifier.Tag.octetString)) as! ASN1UnkeyedDecodingContainerProtocol
//let attribute = try container.decode(ReceiptAttribute.self)
switch type
{
case InAppReceiptField.quantity:
@@ -196,17 +205,21 @@ extension InAppPurchase: ASN1Decodable
case InAppReceiptField.transactionIdentifier:
transactionIdentifier = try valueContainer.decode(String.self)
case InAppReceiptField.purchaseDate:
purchaseDateString = try valueContainer.decode(String.self, template: .universal(ASN1Identifier.Tag.ia5String))
let purchaseDateString = try valueContainer.decode(String.self, template: .universal(ASN1Identifier.Tag.ia5String))
purchaseDate = purchaseDateString.rfc3339date()
case InAppReceiptField.originalTransactionIdentifier:
originalTransactionIdentifier = try valueContainer.decode(String.self)
case InAppReceiptField.originalPurchaseDate:
originalPurchaseDateString = try valueContainer.decode(String.self, template: .universal(ASN1Identifier.Tag.ia5String))
let originalPurchaseDateString = try valueContainer.decode(String.self, template: .universal(ASN1Identifier.Tag.ia5String))
originalPurchaseDate = originalPurchaseDateString.rfc3339date()
case InAppReceiptField.subscriptionExpirationDate:
let str = try valueContainer.decode(String.self, template: .universal(ASN1Identifier.Tag.ia5String))
subscriptionExpirationDateString = str == "" ? nil : str
let subscriptionExpirationDateString = str == "" ? nil : str
subscriptionExpirationDate = subscriptionExpirationDateString?.rfc3339date()
case InAppReceiptField.cancellationDate:
let str = try valueContainer.decode(String.self, template: .universal(ASN1Identifier.Tag.ia5String))
cancellationDateString = str == "" ? nil : str
let cancellationDateString = str == "" ? nil : str
cancellationDate = cancellationDateString?.rfc3339date()
case InAppReceiptField.webOrderLineItemID:
webOrderLineItemID = try valueContainer.decode(Int.self)
case InAppReceiptField.subscriptionTrialPeriod:
@@ -220,6 +233,12 @@ extension InAppPurchase: ASN1Decodable
}
}
}
self.originalTransactionIdentifier = originalTransactionIdentifier
self.productIdentifier = productIdentifier
self.transactionIdentifier = transactionIdentifier
self.purchaseDate = purchaseDate
self.originalPurchaseDate = originalPurchaseDate
}
public static var template: ASN1Template
+18 -4
View File
@@ -3,7 +3,7 @@
// TPReceiptValidator
//
// Created by Pavel Tikhonenko on 28/09/16.
// Copyright © 2016-2020 Pavel Tikhonenko. All rights reserved.
// Copyright © 2016-2021 Pavel Tikhonenko. All rights reserved.
//
import Foundation
@@ -16,6 +16,7 @@ public struct InAppReceiptField
static let appVersion: Int32 = 3
static let opaqueValue: Int32 = 4
static let receiptHash: Int32 = 5 // SHA-1 Hash
static let ageRating: Int32 = 10 // SHA-1 Hash
static let receiptCreationDate: Int32 = 12
static let inAppPurchaseReceipt: Int32 = 17 // The receipt for an in-app purchase.
//TODO: case originalPurchaseDate = 18
@@ -54,6 +55,15 @@ public class InAppReceipt
/// Raw data
private var rawData: Data
/// Initialize a `InAppReceipt` using local receipt
public convenience init() throws
{
let data = try Bundle.main.appStoreReceiptData()
try self.init(receiptData: data)
}
///
///
/// Initialize a `InAppReceipt` with asn1 payload
///
/// - parameter receiptData: `Data` object that represents receipt
@@ -115,7 +125,7 @@ public extension InAppReceipt
/// The date that the app receipt expires
var expirationDate: Date?
{
return payload.expirationDate?.rfc3339date()
return payload.expirationDate
}
/// Returns `true` if any purchases exist, `false` otherwise
@@ -130,12 +140,16 @@ public extension InAppReceipt
return activeAutoRenewableSubscriptionPurchases.count > 0
}
/// The date when the app receipt was created.
var creationDate: Date
{
return payload.creationDate.rfc3339date()!
return payload.creationDate
}
var ageRating: String
{
return payload.ageRating
}
/// In App Receipt in base64
var base64: String
{
+8 -4
View File
@@ -3,7 +3,7 @@
// TPInAppReceipt
//
// Created by Pavel Tikhonenko on 20/01/17.
// Copyright © 2017-2020 Pavel Tikhonenko. All rights reserved.
// Copyright © 2017-2021 Pavel Tikhonenko. All rights reserved.
//
import Foundation
@@ -24,7 +24,7 @@ struct InAppReceiptPayload
let originalAppVersion: String
/// The date that the app receipt expires
let expirationDate: String?
let expirationDate: Date?
/// Used to validate the receipt
let bundleIdentifierData: Data
@@ -36,8 +36,11 @@ struct InAppReceiptPayload
let receiptHash: Data
/// The date when the app receipt was created.
let creationDate: String
let creationDate: Date
/// Age Rating of the app
let ageRating: String
/// Receipt's environment
let environment: String
@@ -46,7 +49,7 @@ struct InAppReceiptPayload
/// Initialize a `InAppReceipt` passing all values
///
init(bundleIdentifier: String, appVersion: String, originalAppVersion: String, purchases: [InAppPurchase], expirationDate: String?, bundleIdentifierData: Data, opaqueValue: Data, receiptHash: Data, creationDate: String, environment: String, rawData: Data)
init(bundleIdentifier: String, appVersion: String, originalAppVersion: String, purchases: [InAppPurchase], expirationDate: Date?, bundleIdentifierData: Data, opaqueValue: Data, receiptHash: Data, creationDate: Date, ageRating: String, environment: String, rawData: Data)
{
self.bundleIdentifier = bundleIdentifier
self.appVersion = appVersion
@@ -57,6 +60,7 @@ struct InAppReceiptPayload
self.opaqueValue = opaqueValue
self.receiptHash = receiptHash
self.creationDate = creationDate
self.ageRating = ageRating
self.environment = environment
self.rawData = rawData
}
+1 -1
View File
@@ -3,7 +3,7 @@
// TPInAppReceipt
//
// Created by Pavel Tikhonenko on 05/02/17.
// Copyright © 2017-2020 Pavel Tikhonenko. All rights reserved.
// Copyright © 2017-2021 Pavel Tikhonenko. All rights reserved.
//
import Foundation
+376
View File
@@ -0,0 +1,376 @@
//
// InAppReceipt+Objc.swift
// TPInAppReceipt
//
// Created by Pavel Tikhonenko on 24.05.2021.
// Copyright © 2020-2021 Pavel Tikhonenko. All rights reserved.
//
import Foundation
import TPInAppReceipt
// MARK: - InAppReceipt
@objc(InAppReceipt) public class InAppReceipt_Objc: NSObject
{
private var wrappedReceipt: InAppReceipt
/// Creates and returns the 'InAppReceipt' instance from data object
///
/// - Returns: 'InAppReceipt' instance
/// - throws: An error in the InAppReceipt domain, if `InAppReceipt` cannot be created.
@objc public class func receipt(from data: Data) throws -> InAppReceipt_Objc
{
return try InAppReceipt_Objc(receiptData: data)
}
/// Creates and returns the 'InAppReceipt' instance from data object
///
/// - Returns: 'InAppReceipt' instance
/// - throws: An error in the InAppReceipt domain, if `InAppReceipt` cannot be created.
@objc public class func receipt(from data: Data) -> InAppReceipt_Objc?
{
return try? InAppReceipt_Objc(receiptData: data)
}
/// Creates and returns the 'InAppReceipt' instance using local receipt
///
/// - Returns: 'InAppReceipt' instance
/// - throws: An error in the InAppReceipt domain, if `InAppReceipt` cannot be created.
@objc public class func local() throws -> InAppReceipt_Objc
{
let data = try Bundle.main.appStoreReceiptData()
return try InAppReceipt_Objc.receipt(from: data)
}
/// Creates and returns the 'InAppReceipt' instance using local receipt
///
/// - Returns: 'InAppReceipt' instance
/// - throws: An error in the InAppReceipt domain, if `InAppReceipt` cannot be created.
@objc public class func local() -> InAppReceipt_Objc?
{
guard let data = try? Bundle.main.appStoreReceiptData() else { return nil }
return InAppReceipt_Objc.receipt(from: data)
}
///
///
/// Initialize a `InAppReceipt` with asn1 payload
///
/// - parameter receiptData: `Data` object that represents receipt
@objc public init(receiptData: Data, rootCertPath: String? = nil) throws
{
self.wrappedReceipt = try InAppReceipt.init(receiptData: receiptData, rootCertPath: rootCertPath)
}
}
@objc public extension InAppReceipt_Objc
{
/// The apps bundle identifier
var bundleIdentifier: String
{
return wrappedReceipt.bundleIdentifier
}
/// The apps version number
var appVersion: String
{
return wrappedReceipt.appVersion
}
/// The version of the app that was originally purchased.
var originalAppVersion: String
{
return wrappedReceipt.originalAppVersion
}
/// In-app purchase's receipts
var purchases: [InAppPurchase_Objc]
{
return wrappedReceipt.purchases.map { .init(purchase: $0) }
}
/// Returns all auto renewable `InAppPurchase`s,
var autoRenewablePurchases: [InAppPurchase_Objc]
{
return wrappedReceipt.purchases.filter({ $0.isRenewableSubscription }).map { .init(purchase: $0) }
}
/// Returns all ACTIVE auto renewable `InAppPurchase`s,
///
var activeAutoRenewableSubscriptionPurchases: [InAppPurchase_Objc]
{
return wrappedReceipt.purchases.filter({ $0.isRenewableSubscription && $0.isActiveAutoRenewableSubscription(forDate: Date()) }).map { .init(purchase: $0) }
}
/// The date that the app receipt expires
var expirationDate: Date?
{
return wrappedReceipt.expirationDate
}
/// Returns `true` if any purchases exist, `false` otherwise
var hasPurchases: Bool
{
return purchases.count > 0
}
/// Returns `true` if any Active Auto Renewable purchases exist, `false` otherwise
var hasActiveAutoRenewablePurchases: Bool
{
return activeAutoRenewableSubscriptionPurchases.count > 0
}
var creationDate: Date
{
return wrappedReceipt.creationDate
}
var ageRating: String
{
return wrappedReceipt.ageRating
}
/// In App Receipt in base64
var base64: String
{
return wrappedReceipt.base64
}
/// Return original transaction identifier if there is a purchase for a specific product identifier
///
/// - parameter productIdentifier: Product name
@objc func originalTransactionIdentifier(ofProductIdentifier productIdentifier: String) -> String?
{
return purchases(ofProductIdentifier: productIdentifier).first?.originalTransactionIdentifier
}
/// Returns `true` if there is a purchase for a specific product identifier, `false` otherwise
///
/// - parameter productIdentifier: Product name
@objc func containsPurchase(ofProductIdentifier productIdentifier: String) -> Bool
{
for item in purchases
{
if item.productIdentifier == productIdentifier
{
return true
}
}
return false
}
/// Returns `[InAppPurchase]` if there are purchases for a specific product identifier,
/// empty array otherwise
///
/// - parameter productIdentifier: Product name
/// - parameter sort: Sorting block
@objc func purchases(ofProductIdentifier productIdentifier: String,
sortedBy sort: ((InAppPurchase_Objc, InAppPurchase_Objc) -> Bool)? = nil) -> [InAppPurchase_Objc]
{
let filtered: [InAppPurchase_Objc] = purchases.filter({
return $0.productIdentifier == productIdentifier
})
if let sort = sort
{
return filtered.sorted(by: {
return sort($0, $1)
})
}else{
return filtered.sorted(by: {
return $0.purchaseDate > $1.purchaseDate
})
}
}
/// Returns `InAppPurchase` if there is a purchase for a specific product identifier,
/// `nil` otherwise
///
/// - parameter productIdentifier: Product name
@objc func activeAutoRenewableSubscriptionPurchases(ofProductIdentifier productIdentifier: String, forDate date: Date) -> InAppPurchase_Objc?
{
let filtered = purchases(ofProductIdentifier: productIdentifier)
for purchase in filtered
{
if purchase.isActiveAutoRenewableSubscription(forDate: date)
{
return purchase
}
}
return nil
}
/// Returns the last `InAppPurchase` if there is one for a specific product identifier,
/// `nil` otherwise
///
/// - parameter productIdentifier: Product name
@objc func lastAutoRenewableSubscriptionPurchase(ofProductIdentifier productIdentifier: String) -> InAppPurchase_Objc?
{
var purchase: InAppPurchase_Objc? = nil
let filtered = purchases(ofProductIdentifier: productIdentifier)
var lastInterval: TimeInterval = 0
for iap in filtered
{
if !(iap.productIdentifier == productIdentifier) {
continue
}
if let thisInterval = iap.subscriptionExpirationDate?.timeIntervalSince1970 {
if purchase == nil || thisInterval > lastInterval {
purchase = iap
lastInterval = thisInterval
}
}
}
return purchase
}
/// Returns true if there is an active subscription for a specific product identifier on the date specified,
/// false otherwise
///
/// - parameter productIdentifier: Product name
/// - parameter date: Date to check subscription against
@objc func hasActiveAutoRenewableSubscription(ofProductIdentifier productIdentifier: String, forDate date: Date) -> Bool
{
return activeAutoRenewableSubscriptionPurchases(ofProductIdentifier: productIdentifier, forDate: date) != nil
}
}
// MARK: - InAppPurchase
@objc(InAppPurchase) public class InAppPurchase_Objc: NSObject
{
@objc public enum `Type`: Int32
{
/// Type that we can't recognize for some reason
case unknown = -1
/// Type that customers purchase once. They don't expire.
case nonConsumable
/// Type that are depleted after one use. Customers can purchase them multiple times.
case consumable
/// Type that customers purchase once and that renew automatically on a recurring basis until customers decide to cancel.
case nonRenewingSubscription
/// Type that customers purchase and it provides access over a limited duration and don't renew automatically. Customers can purchase them again.
case autoRenewableSubscription
}
private let purchase: InAppPurchase
/// The product identifier which purchase related to
@objc public var productIdentifier: String { purchase.productIdentifier }
/// Product type
@objc public var productType: Type { Type(rawValue: purchase.productType.rawValue) ?? .unknown }
/// Transaction identifier
@objc public var transactionIdentifier: String { purchase.transactionIdentifier }
/// Original Transaction identifier
@objc public var originalTransactionIdentifier: String { purchase.originalTransactionIdentifier }
/// Purchase Date
@objc public var purchaseDate: Date { purchase.purchaseDate }
/// Original Purchase Date
@objc public var originalPurchaseDate: Date { purchase.originalPurchaseDate }
/// Subscription Expiration Date. Returns `nil` if the purchase has been expired (in some cases)
@objc public var subscriptionExpirationDate: Date? { purchase.subscriptionExpirationDate }
/// Cancellation Date. Returns `nil` if the purchase is not a renewable subscription
@objc public var cancellationDate: Date? { purchase.cancellationDate }
/// This value is `true`if the customers subscription is currently in the free trial period, or `false` if not.
@objc public var subscriptionTrialPeriod: Bool { purchase.subscriptionTrialPeriod }
/// This value is `true` if the customers subscription is currently in an introductory price period, or `false` if not.
@objc public var subscriptionIntroductoryPricePeriod: Bool { purchase.subscriptionIntroductoryPricePeriod }
/// A unique identifier for purchase events across devices, including subscription-renewal events. This value is the primary key for identifying subscription purchases.
@objc public var webOrderLineItemID: Int { purchase.webOrderLineItemID ?? NSNotFound }
/// The value is an identifier of the subscription offer that the user redeemed.
/// Returns `nil` if the user didn't use any subscription offers.
@objc public var promotionalOfferIdentifier: String? { purchase.promotionalOfferIdentifier }
/// The number of consumable products purchased
/// The default value is `1` unless modified with a mutable payment. The maximum value is 10.
@objc public var quantity: Int { purchase.quantity }
init(purchase: InAppPurchase)
{
self.purchase = purchase
}
}
@objc public extension InAppPurchase_Objc
{
/// A Boolean value indicating whether the purchase is renewable subscription.
@objc var isRenewableSubscription: Bool
{
return purchase.isRenewableSubscription
}
/// Check whether the subscription is active for a specific date
///
/// - Parameter date: The date in which the auto-renewable subscription should be active.
/// - Returns: true if the latest auto-renewable subscription is active for the given date, false otherwise.
@objc func isActiveAutoRenewableSubscription(forDate date: Date) -> Bool
{
return purchase.isActiveAutoRenewableSubscription(forDate: date)
}
}
// MARK: - Validation
/// A InAppReceipt extension helps to validate the receipt
@objc public extension InAppReceipt_Objc
{
/// Verify In App Receipt
///
/// - throws: An error in the InAppReceipt domain, if verification fails
@objc func verify() throws
{
try wrappedReceipt.verifyHash()
try wrappedReceipt.verifyBundleIdentifierAndVersion()
try wrappedReceipt.verifySignature()
}
/// Verify only hash
/// Should be equal to `receiptHash` value
///
/// - throws: An error in the InAppReceipt domain, if verification fails
@objc func verifyHash() throws
{
try wrappedReceipt.verifyHash()
}
/// Verify that the bundle identifier in the receipt matches a hard-coded constant containing the CFBundleIdentifier value you expect in the Info.plist file. If they do not match, validation fails.
/// Verify that the version identifier string in the receipt matches a hard-coded constant containing the CFBundleShortVersionString value (for macOS) or the CFBundleVersion value (for iOS) that you expect in the Info.plist file.
///
///
/// - throws: An error in the InAppReceipt domain, if verification fails
@objc func verifyBundleIdentifierAndVersion() throws
{
try wrappedReceipt.verifyBundleIdentifierAndVersion()
}
/// Verify signature inside pkcs7 container
///
/// - throws: An error in the InAppReceipt domain, if verification can't be completed
@objc func verifySignature() throws
{
try wrappedReceipt.verifySignature()
}
}
+111 -97
View File
@@ -1,9 +1,9 @@
//
// InAppReceiptValidator.swift
// Validation.swift
// TPInAppReceipt
//
// Created by Pavel Tikhonenko on 19/01/17.
// Copyright © 2017-2020 Pavel Tikhonenko. All rights reserved.
// Copyright © 2017-2021 Pavel Tikhonenko. All rights reserved.
//
#if os(iOS) || os(tvOS)
@@ -21,6 +21,19 @@ import CommonCrypto
/// A InAppReceipt extension helps to validate the receipt
public extension InAppReceipt
{
/// Determine whether receipt is valid or not
///
/// - Returns:`true` if the receipt is valid, otherwise `false`
var isValid: Bool
{
do {
try verify()
return true
} catch {
return false
}
}
/// Verify In App Receipt
///
/// - throws: An error in the InAppReceipt domain, if verification fails
@@ -50,35 +63,48 @@ public extension InAppReceipt
/// - throws: An error in the InAppReceipt domain, if verification fails
func verifyBundleIdentifierAndVersion() throws
{
#if targetEnvironment(simulator)
#else
guard let bid = Bundle.main.bundleIdentifier, bid == bundleIdentifier else
{
throw IARError.validationFailed(reason: .bundleIdentifierVerification)
}
#if targetEnvironment(macCatalyst)
guard let v = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String,
v == appVersion else
{
throw IARError.validationFailed(reason: .bundleVersionVerification)
}
#elseif os(iOS) || os(watchOS) || os(tvOS)
guard let v = Bundle.main.infoDictionary?["CFBundleVersion"] as? String,
v == appVersion else
{
throw IARError.validationFailed(reason: .bundleVersionVerification)
}
#elseif os(macOS)
guard let v = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String,
v == appVersion else
{
throw IARError.validationFailed(reason: .bundleVersionVerification)
}
#endif
#endif
try verifyBundleIdentifier()
try verifyBundleVersion()
}
/// Verify that the bundle identifier in the receipt matches a hard-coded constant containing the CFBundleIdentifier value you expect in the Info.plist file. If they do not match, validation fails.
/// Verify that the version identifier string in the receipt matches a hard-coded constant containing the CFBundleShortVersionString value (for macOS) or the CFBundleVersion value (for iOS) that you expect in the Info.plist file.
///
///
/// - throws: An error in the InAppReceipt domain, if verification fails
func verifyBundleIdentifier() throws
{
#if !targetEnvironment(simulator)
guard let bid = Bundle.main.bundleIdentifier, bid == bundleIdentifier else
{
throw IARError.validationFailed(reason: .bundleIdentifierVerification)
}
#endif
}
/// Verify that the version identifier string in the receipt matches a hard-coded constant containing the CFBundleShortVersionString value (for macOS) or the CFBundleVersion value (for iOS) that you expect in the Info.plist file.
///
///
/// - throws: An error in the InAppReceipt domain, if verification fails
func verifyBundleVersion() throws
{
#if !targetEnvironment(simulator)
#if targetEnvironment(macCatalyst) || os(macOS)
guard let v = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String,
v == appVersion else
{
throw IARError.validationFailed(reason: .bundleVersionVerification)
}
#elseif os(iOS) || os(watchOS) || os(tvOS)
guard let v = Bundle.main.infoDictionary?["CFBundleVersion"] as? String,
v == appVersion else
{
throw IARError.validationFailed(reason: .bundleVersionVerification)
}
#endif
#endif
}
/// Verify signature inside pkcs7 container
///
/// - throws: An error in the InAppReceipt domain, if verification can't be completed
@@ -251,15 +277,7 @@ public extension InAppReceipt
fileprivate func guid() -> Data
{
if #available(OSX 11.0, *)
{
if ProcessInfo.processInfo.isiOSAppOnMac || ProcessInfo.processInfo.isMacCatalystApp
{
return macos_guid()
}
}
#if os(watchOS)
var uuidBytes = WKInterfaceDevice.current().identifierForVendor!.uuid
return Data(bytes: &uuidBytes, count: MemoryLayout.size(ofValue: uuidBytes))
@@ -267,64 +285,60 @@ fileprivate func guid() -> Data
var uuidBytes = UIDevice.current.identifierForVendor!.uuid
return Data(bytes: &uuidBytes, count: MemoryLayout.size(ofValue: uuidBytes))
#elseif targetEnvironment(macCatalyst) || os(macOS)
return macos_guid()
var masterPort = mach_port_t()
var kernResult: kern_return_t = IOMasterPort(mach_port_t(MACH_PORT_NULL), &masterPort)
if (kernResult != KERN_SUCCESS)
{
assertionFailure("Failed to initialize master port")
}
let matchingDict = IOBSDNameMatching(masterPort, 0, "en0")
if (matchingDict == nil)
{
assertionFailure("Failed to retrieve guid")
}
var iterator = io_iterator_t()
kernResult = IOServiceGetMatchingServices(masterPort, matchingDict, &iterator)
if (kernResult != KERN_SUCCESS)
{
assertionFailure("Failed to retrieve guid")
}
var guidData: Data?
var service = IOIteratorNext(iterator)
var parentService = io_object_t()
defer
{
IOObjectRelease(iterator)
}
while(service != 0)
{
kernResult = IORegistryEntryGetParentEntry(service, kIOServicePlane, &parentService)
if (kernResult == KERN_SUCCESS)
{
guidData = IORegistryEntryCreateCFProperty(parentService, "IOMACAddress" as CFString, nil, 0).takeRetainedValue() as? Data
IOObjectRelease(parentService)
}
IOObjectRelease(service)
if guidData != nil {
break
}else{
service = IOIteratorNext(iterator)
}
}
if guidData == nil
{
assertionFailure("Failed to retrieve guid")
}
return guidData!
#endif
}
fileprivate func macos_guid() -> Data
{
var masterPort = mach_port_t()
var kernResult: kern_return_t = IOMasterPort(mach_port_t(MACH_PORT_NULL), &masterPort)
if (kernResult != KERN_SUCCESS)
{
assertionFailure("Failed to initialize master port")
}
let matchingDict = IOBSDNameMatching(masterPort, 0, "en0")
if (matchingDict == nil)
{
assertionFailure("Failed to retrieve guid")
}
var iterator = io_iterator_t()
kernResult = IOServiceGetMatchingServices(masterPort, matchingDict, &iterator)
if (kernResult != KERN_SUCCESS)
{
assertionFailure("Failed to retrieve guid")
}
var guidData: Data?
var service = IOIteratorNext(iterator)
var parentService = io_object_t()
defer
{
IOObjectRelease(iterator)
}
while(service != 0)
{
kernResult = IORegistryEntryGetParentEntry(service, kIOServicePlane, &parentService)
if (kernResult == KERN_SUCCESS)
{
guidData = IORegistryEntryCreateCFProperty(parentService, "IOMACAddress" as CFString, nil, 0).takeRetainedValue() as? Data
IOObjectRelease(parentService)
}
IOObjectRelease(service)
if guidData != nil {
break
}else{
service = IOIteratorNext(iterator)
}
}
if guidData == nil
{
assertionFailure("Failed to retrieve guid")
}
return guidData!
}
+29 -20
View File
@@ -1,27 +1,36 @@
Pod::Spec.new do |s|
s.name = "TPInAppReceipt"
s.version = "3.0.0"
s.summary = "Reading and Validating In App Purchase Receipt Locally"
s.description = "A lightweight iOS/OSX library for reading and validating Apple In App Purchase Receipt locally. Pure swift, No OpenSSL!"
s.name = "TPInAppReceipt"
s.version = "3.2.1"
s.summary = "Reading and Validating In App Purchase Receipt Locally"
s.description = "A lightweight iOS/OSX library for reading and validating Apple In App Purchase Receipt locally. Pure swift, No OpenSSL!"
s.homepage = "https://github.com/tikhop/TPInAppReceipt"
s.license = "MIT"
s.source = { :git => "https://github.com/tikhop/TPInAppReceipt.git", :tag => "#{s.version}" }
s.homepage = "https://github.com/tikhop/TPInAppReceipt"
s.license = "MIT"
s.source = { :git => "https://github.com/tikhop/TPInAppReceipt.git", :tag => "#{s.version}" }
s.author = { "Pavel Tikhonenko" => "hi@tikhop.com" }
s.author = { "Pavel Tikhonenko" => "hi@tikhop.com" }
s.swift_versions = ['5.3']
s.ios.deployment_target = '10.0'
s.osx.deployment_target = '10.12'
s.tvos.deployment_target = '10.0'
s.watchos.deployment_target = '6.2'
s.requires_arc = true
s.source_files = "Sources/*.{swift}"
s.resources = "Sources/AppleIncRootCertificate.cer", "Sources/StoreKitTestCertificate.cer"
s.dependency 'ASN1Swift', '~> 1.2.2'
s.swift_versions = ['5.3']
s.ios.deployment_target = '10.0'
s.osx.deployment_target = '10.12'
s.tvos.deployment_target = '10.0'
s.watchos.deployment_target = '6.2'
s.requires_arc = true
s.subspec 'Core' do |core|
core.exclude_files = "Sources/Objc/*.{swift}"
core.source_files = "Sources/*.{swift}"
core.resources = "Sources/AppleIncRootCertificate.cer", "Sources/StoreKitTestCertificate.cer"
core.dependency 'ASN1Swift', '~> 1.2.3'
end
s.subspec 'Objc' do |objc|
objc.source_files = "Sources/Objc/*.{swift}"
objc.dependency 'TPInAppReceipt/Core'
end
s.default_subspecs = 'Core'
end
@@ -15,14 +15,24 @@ class PerformanceTests: XCTestCase
override func setUp()
{
receipt = try! InAppReceipt(receiptData: crashReceipt)
receipt = try! InAppReceipt(receiptData: legacyReceipt)
}
override func tearDown() {
// Put teardown code here. This method is called after the invocation of each test method in the class.
}
func testParsingPerformance() { // / 0.004
// This is an example of a performance test case.
self.measure {
do
{
let receipt = try InAppReceipt(receiptData: legacyReceipt)
}catch{
XCTFail("Unable to parse: \(error)")
}
}
}
func testValidationPerformance() { // / 0.004
// This is an example of a performance test case.
File diff suppressed because one or more lines are too long
@@ -9,11 +9,16 @@ final class TPInAppReceiptTests: XCTestCase {
}
func testCrashReceipts()
{
var r = try? InAppReceipt(receiptData: noOriginalPurchaseDateCrashReceipt)
}
func testNewReceipt()
{
self.measure {
let r = try! InAppReceipt(receiptData: watchReceipt)
//XCTAssert(r.appVersion == 1)
let r = try! InAppReceipt(receiptData: newReceipt)
print(r.creationDate)
}
}