Merge branch 'master' into develop
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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
@@ -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]
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -5,25 +5,27 @@
|
||||
|
||||
# TPInAppReceipt
|
||||
|
||||
[](https://github.com/Carthage/Carthage)
|
||||

|
||||
[](https://cocoapods.org/pods/TPInAppReceipt)
|
||||
[](https://github.com/apple/swift-package-manager)
|
||||
[]()
|
||||
[](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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
@@ -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 customer’s 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
|
||||
{
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
{
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 app’s bundle identifier
|
||||
var bundleIdentifier: String
|
||||
{
|
||||
return wrappedReceipt.bundleIdentifier
|
||||
}
|
||||
|
||||
/// The app’s 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 customer’s 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 customer’s 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
@@ -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
@@ -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)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user