Compare commits
12 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| eb78f3f493 | |||
| 7477d83d4c | |||
| 5393889f39 | |||
| be99440c4a | |||
| bfa0c279cc | |||
| 15144274a9 | |||
| 3928822cbb | |||
| 92409e4498 | |||
| b2764203d4 | |||
| 9817657584 | |||
| a0f87dbc46 | |||
| 20d7a7a2ba |
@@ -1,33 +0,0 @@
|
||||
//
|
||||
// ReceiptRefresher+Convenience.swift
|
||||
// Hekate
|
||||
//
|
||||
// Created by Hannes Oud on 19.09.17.
|
||||
// Copyright © 2017 IdeasOnCanvas GmbH. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import Hekate
|
||||
|
||||
// MARK: - Convenience
|
||||
|
||||
extension ReceiptRefresher {
|
||||
|
||||
public func logIdentifierAndReceipt() {
|
||||
if let deviceIdentifier = LocalReceiptValidator.Parameters.DeviceIdentifier.currentDevice.getData() {
|
||||
print("Device Identifier (Base64):\n" + deviceIdentifier.base64EncodedString())
|
||||
}
|
||||
guard let data = self.receiptData else {
|
||||
print("No receipt")
|
||||
return
|
||||
}
|
||||
let base64 = data.base64EncodedString()
|
||||
print("ReceiptData (Base64):\n" + base64)
|
||||
}
|
||||
|
||||
public var receiptData: Data? {
|
||||
guard let url = Bundle.main.appStoreReceiptURL else { return nil }
|
||||
|
||||
return try? Data(contentsOf: url)
|
||||
}
|
||||
}
|
||||
@@ -1,57 +0,0 @@
|
||||
//
|
||||
// ReceiptRefresher.swift
|
||||
// Hekate
|
||||
//
|
||||
// Created by Hannes Oud on 19.09.17.
|
||||
// Copyright © 2017 IdeasOnCanvas GmbH. All rights reserved.
|
||||
//
|
||||
|
||||
import StoreKit
|
||||
|
||||
@objc
|
||||
public final class ReceiptRefresher: NSObject {
|
||||
|
||||
/// Refreshes the app store receipt using `SKReceiptRefreshRequest(receiptProperties: nil)`.
|
||||
/// The instance of `ReceiptRefresher` on which this is called does not need to be kept around for this to complete.
|
||||
/// - Parameters:
|
||||
/// - queue: Queue on which the completion is called, defaults to main queue.
|
||||
/// - completion: Called after the refresh has completed, gets passed the error on failure.
|
||||
/// - Note: An iTunes Authentication alert will be presented by the system.
|
||||
public func refreshReceipt(queue: DispatchQueue = .main, completion: ((NSError?) -> Void)?) {
|
||||
let handler = RefreshCompletionHandler(queue: queue, completion: completion)
|
||||
let request = SKReceiptRefreshRequest(receiptProperties: nil)
|
||||
request.delegate = handler
|
||||
request.start()
|
||||
}
|
||||
}
|
||||
|
||||
/// Encapsules SKRequestDelegate so it is not exposed at all.
|
||||
///
|
||||
/// - Note: Retains itself until the delegate is called.
|
||||
private final class RefreshCompletionHandler: NSObject, SKRequestDelegate {
|
||||
|
||||
private let completion: ((NSError?) -> Void)?
|
||||
private var retainedSelf: RefreshCompletionHandler?
|
||||
private let queue: DispatchQueue
|
||||
|
||||
init(queue: DispatchQueue, completion: ((NSError?) -> Void)?) {
|
||||
self.completion = completion
|
||||
self.queue = queue
|
||||
super.init()
|
||||
self.retainedSelf = self
|
||||
}
|
||||
|
||||
func requestDidFinish(_ request: SKRequest) {
|
||||
self.queue.async {
|
||||
self.completion?(nil)
|
||||
self.retainedSelf = nil
|
||||
}
|
||||
}
|
||||
|
||||
func request(_ request: SKRequest, didFailWithError error: Error) {
|
||||
self.queue.async {
|
||||
self.completion?(error as NSError)
|
||||
self.retainedSelf = nil
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,11 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="13189.4" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="BYZ-38-t0r">
|
||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="13529" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="BYZ-38-t0r">
|
||||
<device id="retina4_7" orientation="portrait">
|
||||
<adaptation id="fullscreen"/>
|
||||
</device>
|
||||
<dependencies>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="13165.3"/>
|
||||
<deployment identifier="iOS"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="13527"/>
|
||||
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
|
||||
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
||||
</dependencies>
|
||||
@@ -16,59 +17,10 @@
|
||||
<view key="view" contentMode="scaleToFill" id="8bC-Xf-vdC">
|
||||
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
<subviews>
|
||||
<stackView opaque="NO" contentMode="scaleToFill" axis="vertical" translatesAutoresizingMaskIntoConstraints="NO" id="ycc-vI-8g6">
|
||||
<rect key="frame" x="20" y="40" width="335" height="607"/>
|
||||
<subviews>
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="750" verticalCompressionResistancePriority="800" text="Hekate" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="QDJ-Q7-MuR">
|
||||
<rect key="frame" x="0.0" y="0.0" width="335" height="20.5"/>
|
||||
<fontDescription key="fontDescription" type="system" pointSize="17"/>
|
||||
<nil key="textColor"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
<textView clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleToFill" textAlignment="natural" translatesAutoresizingMaskIntoConstraints="NO" id="I7S-AU-qti">
|
||||
<rect key="frame" x="0.0" y="20.5" width="335" height="456.5"/>
|
||||
<color key="backgroundColor" white="1" alpha="1" colorSpace="calibratedWhite"/>
|
||||
<string key="text">Lorem ipsum dolor sit er elit lamet, consectetaur cillium adipisicing pecu, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Nam liber te conscient to factor tum poen legum odioque civiuda.</string>
|
||||
<fontDescription key="fontDescription" type="system" pointSize="14"/>
|
||||
<textInputTraits key="textInputTraits" autocapitalizationType="sentences"/>
|
||||
</textView>
|
||||
<textView clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleToFill" verticalHuggingPriority="255" verticalCompressionResistancePriority="748" text="(no data)" textAlignment="natural" translatesAutoresizingMaskIntoConstraints="NO" id="GRW-kL-w14">
|
||||
<rect key="frame" x="0.0" y="477" width="335" height="100"/>
|
||||
<color key="backgroundColor" white="1" alpha="1" colorSpace="calibratedWhite"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="height" constant="100" id="rkj-Uz-pFJ"/>
|
||||
</constraints>
|
||||
<fontDescription key="fontDescription" type="system" pointSize="14"/>
|
||||
<textInputTraits key="textInputTraits" autocapitalizationType="sentences"/>
|
||||
</textView>
|
||||
<button opaque="NO" contentMode="scaleToFill" verticalHuggingPriority="750" verticalCompressionResistancePriority="850" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="8wd-R7-o0b">
|
||||
<rect key="frame" x="0.0" y="577" width="335" height="30"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="height" constant="30" id="xPJ-u9-TvY"/>
|
||||
</constraints>
|
||||
<state key="normal" title="Refresh Receipt from Store"/>
|
||||
<connections>
|
||||
<action selector="refreshReceiptFromStoreTapped" destination="BYZ-38-t0r" eventType="touchUpInside" id="i9X-hc-Dww"/>
|
||||
</connections>
|
||||
</button>
|
||||
</subviews>
|
||||
</stackView>
|
||||
</subviews>
|
||||
<color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
<constraints>
|
||||
<constraint firstItem="MPn-6g-0qv" firstAttribute="bottom" secondItem="ycc-vI-8g6" secondAttribute="bottom" constant="20" id="J1k-CU-xyi"/>
|
||||
<constraint firstItem="MPn-6g-0qv" firstAttribute="trailing" secondItem="ycc-vI-8g6" secondAttribute="trailing" constant="20" id="KiK-CX-iz8"/>
|
||||
<constraint firstItem="ycc-vI-8g6" firstAttribute="leading" secondItem="MPn-6g-0qv" secondAttribute="leading" constant="20" id="ZbF-Cm-dp2"/>
|
||||
<constraint firstItem="ycc-vI-8g6" firstAttribute="top" secondItem="MPn-6g-0qv" secondAttribute="top" constant="20" id="r2h-em-aKZ"/>
|
||||
</constraints>
|
||||
<edgeInsets key="layoutMargins" top="20" left="20" bottom="20" right="20"/>
|
||||
<viewLayoutGuide key="safeArea" id="MPn-6g-0qv"/>
|
||||
</view>
|
||||
<connections>
|
||||
<outlet property="receiptDataTextView" destination="GRW-kL-w14" id="zMl-tu-gie"/>
|
||||
<outlet property="textView" destination="I7S-AU-qti" id="PPF-3A-8xS"/>
|
||||
</connections>
|
||||
</viewController>
|
||||
<placeholder placeholderIdentifier="IBFirstResponder" id="dkx-z0-nzr" sceneMemberID="firstResponder"/>
|
||||
</objects>
|
||||
|
||||
@@ -1,53 +0,0 @@
|
||||
//
|
||||
// HekateDemoViewModel.swift
|
||||
// Hekate Demo iOS
|
||||
//
|
||||
// Created by Hannes Oud on 08.09.17.
|
||||
// Copyright © 2017 IdeasOnCanvas GmbH. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import Hekate
|
||||
|
||||
struct HekateDemoViewModel {
|
||||
|
||||
var hasReceipt: Bool { return self.lastReceiptData != nil }
|
||||
var lastReceiptData: Data?
|
||||
var lastValidationResult: LocalReceiptValidator.Result?
|
||||
var refreshError: NSError?
|
||||
var receiptIsValid: Bool {
|
||||
guard let result = self.lastValidationResult else { return false }
|
||||
|
||||
switch result {
|
||||
case .success:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
var descriptionText: String {
|
||||
if let refreshError = refreshError {
|
||||
return "Refresh Issue: " + refreshError.localizedDescription
|
||||
}
|
||||
|
||||
guard let result = self.lastValidationResult else { return "(No result)" }
|
||||
|
||||
switch result {
|
||||
case .success(let receipt):
|
||||
return "Valid\n" + receipt.description
|
||||
case .error(let error):
|
||||
return "Invalid: \(error)"
|
||||
}
|
||||
}
|
||||
var receiptDataBase64Text: String {
|
||||
guard let data = self.lastReceiptData else { return "(no data)" }
|
||||
|
||||
return data.base64EncodedString(options: [.lineLength64Characters])
|
||||
}
|
||||
|
||||
mutating func update() {
|
||||
self.lastReceiptData = LocalReceiptValidator.Parameters.ReceiptOrigin.installedInMainBundle.loadData()
|
||||
self.lastValidationResult = LocalReceiptValidator().validateReceipt()
|
||||
}
|
||||
|
||||
}
|
||||
@@ -6,39 +6,6 @@
|
||||
// Copyright © 2017 IdeasOnCanvas GmbH. All rights reserved.
|
||||
//
|
||||
|
||||
import Hekate
|
||||
import StoreKit
|
||||
import UIKit
|
||||
|
||||
class ViewController: UIViewController {
|
||||
|
||||
private var receiptRefresher = ReceiptRefresher()
|
||||
private var viewModel = HekateDemoViewModel() {
|
||||
didSet {
|
||||
self.updateViewFromViewModel()
|
||||
}
|
||||
}
|
||||
@IBOutlet private weak var textView: UITextView!
|
||||
@IBOutlet private weak var receiptDataTextView: UITextView!
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
updateViewModel()
|
||||
}
|
||||
|
||||
private func updateViewFromViewModel() {
|
||||
textView.text = self.viewModel.descriptionText
|
||||
receiptDataTextView.text = self.viewModel.receiptDataBase64Text
|
||||
}
|
||||
|
||||
private func updateViewModel() {
|
||||
viewModel.update()
|
||||
}
|
||||
|
||||
@IBAction func refreshReceiptFromStoreTapped() {
|
||||
receiptRefresher.refreshReceipt { error in
|
||||
self.viewModel.refreshError = error
|
||||
self.updateViewFromViewModel()
|
||||
}
|
||||
}
|
||||
}
|
||||
class ViewController: UIViewController {}
|
||||
|
||||
@@ -277,7 +277,7 @@ class LocalReceiptValidationTests: XCTestCase {
|
||||
func testiOSParsingPerformance() {
|
||||
guard let data = assertB64TestAsset(filename: "mindnode_ios_michaelsandbox_receipt1.b64") else { return }
|
||||
|
||||
let parameters = LocalReceiptValidator.Parameters.allSteps.with {
|
||||
let parameters = LocalReceiptValidator.Parameters.default.with {
|
||||
$0.receiptOrigin = .data(data)
|
||||
$0.deviceIdentifier = LocalReceiptValidator.Parameters.DeviceIdentifier(uuid: UUID(uuidString: "3B76A7BD-8F5B-46A4-BCB1-CCE8DBD1B3CD")!)
|
||||
}
|
||||
@@ -286,3 +286,13 @@ class LocalReceiptValidationTests: XCTestCase {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - LocalReceiptValidator + Convenience
|
||||
|
||||
extension LocalReceiptValidator {
|
||||
|
||||
/// Validates a local receipt and returns the result using the parameters `LocalReceiptValidator.Parameters.default`, which can be further configured in the passed block.
|
||||
func validateReceipt(configuration: (inout Parameters) -> Void) -> Result {
|
||||
return validateReceipt(parameters: Parameters.default.with(block: configuration))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,7 +32,6 @@
|
||||
D15358B41F62C47400F297D0 /* deprecatedSinglesTypeExpiredAppleCert_receipt.b64 in Resources */ = {isa = PBXBuildFile; fileRef = D15358B11F62C3C400F297D0 /* deprecatedSinglesTypeExpiredAppleCert_receipt.b64 */; };
|
||||
D15358B51F62C47500F297D0 /* deprecatedSinglesTypeExpiredAppleCert_receipt.b64 in Resources */ = {isa = PBXBuildFile; fileRef = D15358B11F62C3C400F297D0 /* deprecatedSinglesTypeExpiredAppleCert_receipt.b64 */; };
|
||||
D15358EF1F62D2C100F297D0 /* Hekate.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = D1D6F4B51F5D684C00E86FE1 /* Hekate.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
|
||||
D15358F11F62D43400F297D0 /* HekateDemoViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D15358F01F62D43400F297D0 /* HekateDemoViewModel.swift */; };
|
||||
D15C59111F697D4D006F66FE /* pkcs7_union_accessors.h in Headers */ = {isa = PBXBuildFile; fileRef = D19095BA1F6004D10095729B /* pkcs7_union_accessors.h */; settings = {ATTRIBUTES = (Private, ); }; };
|
||||
D15C59151F698061006F66FE /* aes.h in Headers */ = {isa = PBXBuildFile; fileRef = D1D431061F69627600F7F39D /* aes.h */; settings = {ATTRIBUTES = (Private, ); }; };
|
||||
D15C59181F6981C4006F66FE /* asn1.h in Headers */ = {isa = PBXBuildFile; fileRef = D1D431071F69627600F7F39D /* asn1.h */; settings = {ATTRIBUTES = (Private, ); }; };
|
||||
@@ -223,10 +222,6 @@
|
||||
D1D6F4F11F5D691400E86FE1 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = D1D6F4EF1F5D691400E86FE1 /* LaunchScreen.storyboard */; };
|
||||
D1D6F53F1F5D89D000E86FE1 /* LocalReceiptValidator.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1D6F53E1F5D89D000E86FE1 /* LocalReceiptValidator.swift */; };
|
||||
D1D6F5401F5D89D800E86FE1 /* LocalReceiptValidator.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1D6F53E1F5D89D000E86FE1 /* LocalReceiptValidator.swift */; };
|
||||
D1DD22421F711E5E00D111F4 /* ReceiptRefresher.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1DD22411F711E5E00D111F4 /* ReceiptRefresher.swift */; };
|
||||
D1DD22431F711E5E00D111F4 /* ReceiptRefresher.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1DD22411F711E5E00D111F4 /* ReceiptRefresher.swift */; };
|
||||
D1DD22451F7121FE00D111F4 /* ReceiptRefresher+Convenience.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1DD22441F7121FE00D111F4 /* ReceiptRefresher+Convenience.swift */; };
|
||||
D1DD22461F7121FE00D111F4 /* ReceiptRefresher+Convenience.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1DD22441F7121FE00D111F4 /* ReceiptRefresher+Convenience.swift */; };
|
||||
D1FE343D1F604F020029576B /* Receipt.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1FE343C1F604F020029576B /* Receipt.swift */; };
|
||||
D1FE343E1F604F020029576B /* Receipt.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1FE343C1F604F020029576B /* Receipt.swift */; };
|
||||
D1FE34401F604F540029576B /* LocalReceiptValidator+Parameters.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1FE343F1F604F540029576B /* LocalReceiptValidator+Parameters.swift */; };
|
||||
@@ -291,7 +286,6 @@
|
||||
D150A0ED1F669A880026ED04 /* LocalReceiptValidationInAppPurchaseTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalReceiptValidationInAppPurchaseTests.swift; sourceTree = "<group>"; };
|
||||
D15358A51F62BEC100F297D0 /* grandUnifiedExpiredAppleCert_receipt.b64 */ = {isa = PBXFileReference; lastKnownFileType = text; path = grandUnifiedExpiredAppleCert_receipt.b64; sourceTree = "<group>"; };
|
||||
D15358B11F62C3C400F297D0 /* deprecatedSinglesTypeExpiredAppleCert_receipt.b64 */ = {isa = PBXFileReference; lastKnownFileType = text; path = deprecatedSinglesTypeExpiredAppleCert_receipt.b64; sourceTree = "<group>"; };
|
||||
D15358F01F62D43400F297D0 /* HekateDemoViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HekateDemoViewModel.swift; sourceTree = "<group>"; };
|
||||
D15C59141F698005006F66FE /* Hekate.modulemap */ = {isa = PBXFileReference; lastKnownFileType = "sourcecode.module-map"; path = Hekate.modulemap; sourceTree = "<group>"; };
|
||||
D19095811F6000A40095729B /* Hekate Demo macOS.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "Hekate Demo macOS.app"; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
D19095831F6000A40095729B /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
|
||||
@@ -483,8 +477,6 @@
|
||||
D1D6F5471F5D8DF700E86FE1 /* hannes_mac_mindnode_pro_Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = hannes_mac_mindnode_pro_Info.plist; sourceTree = "<group>"; };
|
||||
D1D6F5491F5D9B1F00E86FE1 /* TestAssetLoading.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestAssetLoading.swift; sourceTree = "<group>"; };
|
||||
D1D6F54F1F5D9E8D00E86FE1 /* not_a_receipt */ = {isa = PBXFileReference; lastKnownFileType = text; path = not_a_receipt; sourceTree = "<group>"; };
|
||||
D1DD22411F711E5E00D111F4 /* ReceiptRefresher.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReceiptRefresher.swift; sourceTree = "<group>"; };
|
||||
D1DD22441F7121FE00D111F4 /* ReceiptRefresher+Convenience.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ReceiptRefresher+Convenience.swift"; sourceTree = "<group>"; };
|
||||
D1FE343C1F604F020029576B /* Receipt.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Receipt.swift; sourceTree = "<group>"; };
|
||||
D1FE343F1F604F540029576B /* LocalReceiptValidator+Parameters.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "LocalReceiptValidator+Parameters.swift"; sourceTree = "<group>"; };
|
||||
/* End PBXFileReference section */
|
||||
@@ -854,7 +846,6 @@
|
||||
D1D6F4FC1F5D696800E86FE1 /* Hekate */,
|
||||
D1D6F4E51F5D691400E86FE1 /* Hekate Demo iOS */,
|
||||
D19095821F6000A40095729B /* Hekate Demo macOS */,
|
||||
D1DD22401F711E4400D111F4 /* Hekate Demo Shared */,
|
||||
D19095951F6000A40095729B /* Hekate Tests macOS */,
|
||||
D19095AA1F6001800095729B /* Hekate Tests iOS */,
|
||||
D19095B61F6001C20095729B /* Hekate Tests Shared */,
|
||||
@@ -880,7 +871,6 @@
|
||||
children = (
|
||||
D1D6F4E61F5D691400E86FE1 /* AppDelegate.swift */,
|
||||
D1D6F4E81F5D691400E86FE1 /* ViewController.swift */,
|
||||
D15358F01F62D43400F297D0 /* HekateDemoViewModel.swift */,
|
||||
D1D6F4EA1F5D691400E86FE1 /* Main.storyboard */,
|
||||
D1D6F4ED1F5D691400E86FE1 /* Assets.xcassets */,
|
||||
D1D6F4EF1F5D691400E86FE1 /* LaunchScreen.storyboard */,
|
||||
@@ -955,15 +945,6 @@
|
||||
path = Tools;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
D1DD22401F711E4400D111F4 /* Hekate Demo Shared */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
D1DD22411F711E5E00D111F4 /* ReceiptRefresher.swift */,
|
||||
D1DD22441F7121FE00D111F4 /* ReceiptRefresher+Convenience.swift */,
|
||||
);
|
||||
path = "Hekate Demo Shared";
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
/* End PBXGroup section */
|
||||
|
||||
/* Begin PBXHeadersBuildPhase section */
|
||||
@@ -1419,10 +1400,8 @@
|
||||
isa = PBXSourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
D1DD22431F711E5E00D111F4 /* ReceiptRefresher.swift in Sources */,
|
||||
D19095861F6000A40095729B /* ViewController.swift in Sources */,
|
||||
D19095841F6000A40095729B /* AppDelegate.swift in Sources */,
|
||||
D1DD22461F7121FE00D111F4 /* ReceiptRefresher+Convenience.swift in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
@@ -1488,9 +1467,6 @@
|
||||
files = (
|
||||
D1D6F4E91F5D691400E86FE1 /* ViewController.swift in Sources */,
|
||||
D1D6F4E71F5D691400E86FE1 /* AppDelegate.swift in Sources */,
|
||||
D15358F11F62D43400F297D0 /* HekateDemoViewModel.swift in Sources */,
|
||||
D1DD22421F711E5E00D111F4 /* ReceiptRefresher.swift in Sources */,
|
||||
D1DD22451F7121FE00D111F4 /* ReceiptRefresher+Convenience.swift in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
|
||||
@@ -11,9 +11,11 @@ import Foundation
|
||||
public extension LocalReceiptValidator {
|
||||
|
||||
/// Describes how to validate a receipt, and how/where to obtain the dependencies (receipt, deviceIdentifier, apple root certificate)
|
||||
/// Use .allSteps to initialize the standard parameters. By default, no `propertyValidations` are active.
|
||||
/// Use .default to initialize the standard parameters. By default, no `propertyValidations` are active.
|
||||
public struct Parameters {
|
||||
|
||||
// MARK: - Properties
|
||||
|
||||
public var receiptOrigin: ReceiptOrigin = .installedInMainBundle
|
||||
public var shouldValidateSignaturePresence: Bool = true
|
||||
public var shouldValidateSignatureAuthenticity: Bool = true
|
||||
@@ -22,19 +24,31 @@ public extension LocalReceiptValidator {
|
||||
public var rootCertificateOrigin: RootCertificateOrigin = .cerFileBundledWithHekate
|
||||
public var propertyValidations: [PropertyValidation] = []
|
||||
|
||||
// MARK: - Lifecycle
|
||||
|
||||
/// Or use .default to initialize a sensible defaults
|
||||
public init(receiptOrigin: ReceiptOrigin, shouldValidateSignaturePresence: Bool, shouldValidateSignatureAuthenticity: Bool, shouldValidateHash: Bool, deviceIdentifier: DeviceIdentifier, rootCertificateOrigin: RootCertificateOrigin, propertyValidations: [PropertyValidation]) {
|
||||
self.receiptOrigin = receiptOrigin
|
||||
self.shouldValidateSignaturePresence = shouldValidateSignaturePresence
|
||||
self.shouldValidateSignatureAuthenticity = shouldValidateSignatureAuthenticity
|
||||
self.shouldValidateHash = shouldValidateHash
|
||||
self.deviceIdentifier = deviceIdentifier
|
||||
self.rootCertificateOrigin = rootCertificateOrigin
|
||||
}
|
||||
|
||||
/// Either use `.default` to get a default preset, or specify everything via the complete init(…) with all parameters.
|
||||
private init() {}
|
||||
|
||||
public static var `default`: Parameters {
|
||||
return Parameters()
|
||||
}
|
||||
|
||||
/// Configure an instance with a block
|
||||
public func with(block: (inout Parameters) -> Void) -> Parameters {
|
||||
var copy = self
|
||||
block(©)
|
||||
return copy
|
||||
}
|
||||
|
||||
/// Use .allSteps to initialize
|
||||
private init() {}
|
||||
|
||||
public static var allSteps: Parameters {
|
||||
return Parameters()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -23,16 +23,15 @@ public struct LocalReceiptValidator {
|
||||
|
||||
// MARK: - Local Receipt Validation
|
||||
|
||||
/// Validates a local receipt and returns the result using the parameters `LocalReceiptValidator.Parameters.allSteps`, which can be further configured in the passed block.
|
||||
public func validateReceipt(configuration: (inout Parameters) -> Void) -> Result {
|
||||
return validateReceipt(parameters: Parameters.allSteps.with(block: configuration))
|
||||
}
|
||||
|
||||
/// Validates a local receipt and returns the result using the passed parameters.
|
||||
public func validateReceipt(parameters: Parameters = Parameters.allSteps) -> Result {
|
||||
public func validateReceipt(parameters: Parameters = Parameters.default) -> Result {
|
||||
var data: Data?
|
||||
var deviceIdData: Data?
|
||||
do {
|
||||
deviceIdData = parameters.deviceIdentifier.getData()
|
||||
guard let receiptData = parameters.receiptOrigin.loadData() else { throw Error.couldNotFindReceipt }
|
||||
|
||||
data = receiptData
|
||||
let receiptContainer = try self.extractPKCS7Container(data: receiptData)
|
||||
|
||||
if parameters.shouldValidateSignaturePresence {
|
||||
@@ -49,14 +48,14 @@ public struct LocalReceiptValidator {
|
||||
try self.validateProperties(receipt: receipt, validations: parameters.propertyValidations)
|
||||
|
||||
if parameters.shouldValidateHash {
|
||||
guard let deviceIdentifierData = parameters.deviceIdentifier.getData() else { throw Error.deviceIdentifierNotDeterminable }
|
||||
guard let deviceIdentifierData = deviceIdData else { throw Error.deviceIdentifierNotDeterminable }
|
||||
|
||||
try self.validateHash(receipt: receipt, deviceIdentifierData: deviceIdentifierData)
|
||||
}
|
||||
return .success(receipt)
|
||||
return .success(receipt, receiptData: receiptData, deviceIdentifier: deviceIdData)
|
||||
} catch {
|
||||
assert(error is LocalReceiptValidator.Error)
|
||||
return .error(error as? LocalReceiptValidator.Error ?? .unknown)
|
||||
return .error(error as? LocalReceiptValidator.Error ?? .unknown, receiptData: data, deviceIdentifier: deviceIdData)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -94,7 +93,7 @@ public struct LocalReceiptValidator {
|
||||
private extension LocalReceiptValidator {
|
||||
|
||||
func validateHash(receipt: Receipt, deviceIdentifierData: Data) throws {
|
||||
// Make sure that the ParsedReceipt instances has non-nil values needed for hash comparison
|
||||
// Make sure that the Receipt instances has non-nil values needed for hash comparison
|
||||
guard let receiptOpaqueValueData = receipt.opaqueValue else { throw Error.incorrectHash }
|
||||
guard let receiptBundleIdData = receipt.bundleIdData else { throw Error.incorrectHash }
|
||||
guard let receiptHashData = receipt.sha1Hash else { throw Error.incorrectHash }
|
||||
@@ -193,65 +192,64 @@ private extension LocalReceiptValidator {
|
||||
guard let contents = pkcs7.pkcs7.pointee.d.sign.pointee.contents, let octets = contents.pointee.d.data else { throw Error.malformedReceipt }
|
||||
guard let initialPointer = UnsafePointer(octets.pointee.data) else { throw Error.malformedReceipt }
|
||||
let length = Int(octets.pointee.length)
|
||||
var parsedReceipt = Receipt()
|
||||
var receipt = Receipt()
|
||||
|
||||
try self.parseASN1Set(pointer: initialPointer, length: length) { attributeType, value in
|
||||
guard let attribute = KnownReceiptAttribute(rawValue: attributeType) else { return }
|
||||
|
||||
switch attribute {
|
||||
case .bundleIdentifier:
|
||||
parsedReceipt.bundleIdData = value.dataValue
|
||||
parsedReceipt.bundleIdentifier = value.unwrappedStringValue
|
||||
receipt.bundleIdData = value.dataValue
|
||||
receipt.bundleIdentifier = value.unwrappedStringValue
|
||||
case .appVersion:
|
||||
parsedReceipt.appVersion = value.unwrappedStringValue
|
||||
receipt.appVersion = value.unwrappedStringValue
|
||||
case .opaqueValue:
|
||||
parsedReceipt.opaqueValue = value.dataValue
|
||||
receipt.opaqueValue = value.dataValue
|
||||
case .sha1Hash:
|
||||
parsedReceipt.sha1Hash = value.dataValue
|
||||
receipt.sha1Hash = value.dataValue
|
||||
case .inAppPurchaseReceipts:
|
||||
guard let pointer = value.valuePointer else { break }
|
||||
|
||||
let iapReceipt = try parseInAppPurchaseReceipt(pointer: pointer, length: value.length)
|
||||
parsedReceipt.inAppPurchaseReceipts.append(iapReceipt)
|
||||
receipt.inAppPurchaseReceipts.append(iapReceipt)
|
||||
case .receiptCreationDate:
|
||||
parsedReceipt.receiptCreationDate = value.unwrappedDateValue
|
||||
receipt.receiptCreationDate = value.unwrappedDateValue
|
||||
case .originalAppVersion:
|
||||
parsedReceipt.originalAppVersion = value.unwrappedStringValue
|
||||
receipt.originalAppVersion = value.unwrappedStringValue
|
||||
case .expirationDate:
|
||||
parsedReceipt.expirationDate = value.unwrappedDateValue
|
||||
break
|
||||
receipt.expirationDate = value.unwrappedDateValue
|
||||
}
|
||||
}
|
||||
return parsedReceipt
|
||||
return receipt
|
||||
}
|
||||
|
||||
private func parseInAppPurchaseReceipt(pointer: UnsafePointer<UInt8>, length: Int) throws -> InAppPurchaseReceipt {
|
||||
var parsedInAppPurchaseReceipt = InAppPurchaseReceipt()
|
||||
var inAppPurchaseReceipt = InAppPurchaseReceipt()
|
||||
try self.parseASN1Set(pointer: pointer, length: length) { attributeType, value in
|
||||
guard let attribute = KnownInAppPurchaseAttribute(rawValue: attributeType) else { return }
|
||||
|
||||
switch attribute {
|
||||
case .quantity:
|
||||
parsedInAppPurchaseReceipt.quantity = value.intValue
|
||||
inAppPurchaseReceipt.quantity = value.intValue
|
||||
case .productIdentifier:
|
||||
parsedInAppPurchaseReceipt.productIdentifier = value.unwrappedStringValue
|
||||
inAppPurchaseReceipt.productIdentifier = value.unwrappedStringValue
|
||||
case .transactionIdentifier:
|
||||
parsedInAppPurchaseReceipt.transactionIdentifier = value.unwrappedStringValue
|
||||
inAppPurchaseReceipt.transactionIdentifier = value.unwrappedStringValue
|
||||
case .originalTransactionIdentifier:
|
||||
parsedInAppPurchaseReceipt.originalTransactionIdentifier = value.unwrappedStringValue
|
||||
inAppPurchaseReceipt.originalTransactionIdentifier = value.unwrappedStringValue
|
||||
case .purchaseDate:
|
||||
parsedInAppPurchaseReceipt.purchaseDate = value.unwrappedDateValue
|
||||
inAppPurchaseReceipt.purchaseDate = value.unwrappedDateValue
|
||||
case .originalPurchaseDate:
|
||||
parsedInAppPurchaseReceipt.originalPurchaseDate = value.unwrappedDateValue
|
||||
inAppPurchaseReceipt.originalPurchaseDate = value.unwrappedDateValue
|
||||
case .subscriptionExpirationDate:
|
||||
parsedInAppPurchaseReceipt.subscriptionExpirationDate = value.unwrappedDateValue
|
||||
inAppPurchaseReceipt.subscriptionExpirationDate = value.unwrappedDateValue
|
||||
case .cancellationDate:
|
||||
parsedInAppPurchaseReceipt.cancellationDate = value.unwrappedDateValue
|
||||
inAppPurchaseReceipt.cancellationDate = value.unwrappedDateValue
|
||||
case .webOrderLineItemId:
|
||||
parsedInAppPurchaseReceipt.webOrderLineItemId = value.intValue
|
||||
inAppPurchaseReceipt.webOrderLineItemId = value.intValue
|
||||
}
|
||||
}
|
||||
return parsedInAppPurchaseReceipt
|
||||
return inAppPurchaseReceipt
|
||||
}
|
||||
|
||||
private func parseASN1Set(pointer initialPointer: UnsafePointer<UInt8>, length: Int, valueAttributeAction: (_ attributeType: Int32, _ value: ASN1Object) throws -> Void) throws {
|
||||
@@ -284,7 +282,7 @@ private extension LocalReceiptValidator {
|
||||
|
||||
private extension LocalReceiptValidator {
|
||||
|
||||
/// See ParsedReceipt.swift for details and a link to Apple reference
|
||||
/// See Receipt.swift for details and a link to Apple reference
|
||||
enum KnownReceiptAttribute: Int32 {
|
||||
case bundleIdentifier = 2
|
||||
case appVersion = 3
|
||||
@@ -303,7 +301,7 @@ private extension LocalReceiptValidator {
|
||||
// - and of unknown type 14(L=3), 25(L=3), 11(L=4), 13(L=4), 1(L=6), 9(L=6), 16(L=6), 15(L=8), 7(L=66), 6(L=69 variable)
|
||||
}
|
||||
|
||||
/// See ParsedReceipt.swift for details and a link to Apple reference
|
||||
/// See Receipt.swift for details and a link to Apple reference
|
||||
enum KnownInAppPurchaseAttribute: Int32 {
|
||||
case quantity = 1701
|
||||
case productIdentifier = 1702
|
||||
@@ -323,12 +321,12 @@ extension LocalReceiptValidator {
|
||||
|
||||
public enum Result {
|
||||
|
||||
case success(Receipt)
|
||||
case error(LocalReceiptValidator.Error)
|
||||
case success(Receipt, receiptData: Data, deviceIdentifier: Data?)
|
||||
case error(LocalReceiptValidator.Error, receiptData: Data?, deviceIdentifier: Data?)
|
||||
|
||||
public var receipt: Receipt? {
|
||||
switch self {
|
||||
case .success(let receipt):
|
||||
case .success(let receipt, _, _):
|
||||
return receipt
|
||||
case .error:
|
||||
return nil
|
||||
@@ -339,10 +337,30 @@ extension LocalReceiptValidator {
|
||||
switch self {
|
||||
case .success:
|
||||
return nil
|
||||
case .error(let error):
|
||||
case .error(let error, _, _):
|
||||
return error
|
||||
}
|
||||
}
|
||||
|
||||
/// The receipt data if it could be loaded
|
||||
public var receiptData: Data? {
|
||||
switch self {
|
||||
case .success(_, let data, _):
|
||||
return data
|
||||
case .error(_, let data, _):
|
||||
return data
|
||||
}
|
||||
}
|
||||
|
||||
/// The device identifier if it could be determined
|
||||
public var deviceIdentifier: Data? {
|
||||
switch self {
|
||||
case .success(_, _, let data):
|
||||
return data
|
||||
case .error(_, _, let data):
|
||||
return data
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -76,7 +76,7 @@ extension Receipt: AutoEquatable {}
|
||||
|
||||
// MARK: - CustomStringConvertible
|
||||
|
||||
extension Receipt: CustomStringConvertible {
|
||||
extension Receipt: CustomStringConvertible, CustomDebugStringConvertible {
|
||||
|
||||
public var description: String {
|
||||
let formatter = StringFormatter()
|
||||
@@ -91,11 +91,15 @@ extension Receipt: CustomStringConvertible {
|
||||
("expirationDate", formatter.format(self.expirationDate)),
|
||||
("inAppPurchaseReceipts", formatter.format(self.inAppPurchaseReceipts))
|
||||
]
|
||||
return "ParsedReceipt(\n" + formatter.format(props) + "\n)"
|
||||
return "Receipt(\n" + formatter.format(props) + "\n)"
|
||||
}
|
||||
|
||||
public var debugDescription: String {
|
||||
return description
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - ParsedInAppPurchaseReceipt
|
||||
// MARK: - InAppPurchaseReceipt
|
||||
|
||||
/// An In-App-Purchase Receipt as Parsed from a receipt file.
|
||||
///
|
||||
@@ -169,7 +173,7 @@ public struct InAppPurchaseReceipt {
|
||||
/// This value is a unique ID that identifies purchase events across devices, including subscription renewal purchase events.
|
||||
public internal(set) var webOrderLineItemId: Int?
|
||||
|
||||
/// For documentation see ParsedInAppPurchaseReceipt itself.
|
||||
/// For documentation see InAppPurchaseReceipt itself.
|
||||
public init(quantity: Int?, productIdentifier: String?, transactionIdentifier: String?, originalTransactionIdentifier: String?, purchaseDate: Date?, originalPurchaseDate: Date?, subscriptionExpirationDate: Date?, cancellationDate: Date?, webOrderLineItemId: Int?) {
|
||||
self.quantity = quantity
|
||||
self.productIdentifier = productIdentifier
|
||||
@@ -191,7 +195,7 @@ extension InAppPurchaseReceipt: AutoEquatable {}
|
||||
|
||||
// MARK: - CustomStringConvertible
|
||||
|
||||
extension InAppPurchaseReceipt: CustomStringConvertible {
|
||||
extension InAppPurchaseReceipt: CustomStringConvertible, CustomDebugStringConvertible {
|
||||
|
||||
public var description: String {
|
||||
let formatter = StringFormatter()
|
||||
@@ -206,7 +210,11 @@ extension InAppPurchaseReceipt: CustomStringConvertible {
|
||||
("cancellationDate", formatter.format(self.cancellationDate)),
|
||||
("webOrderLineItemId", formatter.format(self.webOrderLineItemId))
|
||||
]
|
||||
return "ParsedInAppPurchaseReceipt(\n" + formatter.format(props) + "\n)"
|
||||
return "InAppPurchaseReceipt(\n" + formatter.format(props) + "\n)"
|
||||
}
|
||||
|
||||
public var debugDescription: String {
|
||||
return description
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -23,7 +23,7 @@ private func compareArrays<T>(lhs: [T], rhs: [T], compare: (_ lhs: T, _ rhs: T)
|
||||
}
|
||||
|
||||
// MARK: - AutoEquatable for classes, protocols, structs
|
||||
// MARK: - ParsedInAppPurchaseReceipt AutoEquatable
|
||||
// MARK: - InAppPurchaseReceipt AutoEquatable
|
||||
extension InAppPurchaseReceipt: Equatable {}
|
||||
public func == (lhs: InAppPurchaseReceipt, rhs: InAppPurchaseReceipt) -> Bool {
|
||||
guard compareOptionals(lhs: lhs.quantity, rhs: rhs.quantity, compare: ==) else { return false }
|
||||
@@ -37,7 +37,7 @@ public func == (lhs: InAppPurchaseReceipt, rhs: InAppPurchaseReceipt) -> Bool {
|
||||
guard compareOptionals(lhs: lhs.webOrderLineItemId, rhs: rhs.webOrderLineItemId, compare: ==) else { return false }
|
||||
return true
|
||||
}
|
||||
// MARK: - ParsedReceipt AutoEquatable
|
||||
// MARK: - Receipt AutoEquatable
|
||||
extension Receipt: Equatable {}
|
||||
public func == (lhs: Receipt, rhs: Receipt) -> Bool {
|
||||
guard compareOptionals(lhs: lhs.bundleIdentifier, rhs: rhs.bundleIdentifier, compare: ==) else { return false }
|
||||
|
||||
@@ -36,11 +36,11 @@ switch result {
|
||||
|
||||
### Customize validation dependencies or steps
|
||||
|
||||
Take `LocalReceiptValidator.Parameters.allSteps` and customize it, then pass it to `validateReceipt(parameters:)`, or use the shortcut which takes a configuration block, like so:
|
||||
Take `LocalReceiptValidator.Parameters.default` and customize it, then pass it to `validateReceipt(parameters:)`, like so:
|
||||
|
||||
```swift
|
||||
// Customizing validation parameters with configuration block, base on .allSteps
|
||||
let result = receiptValidator.validateReceipt {
|
||||
// Customizing validation parameters with configuration block, base on .default
|
||||
let parameters = LocalReceiptValidator.Parameters.default.with {
|
||||
$0.receiptOrigin = .data(myData)
|
||||
$0.shouldValidateSignaturePresence = false // skip signature presence validation
|
||||
$0.shouldValidateSignatureAuthenticity = false // skip signature authenticity validation
|
||||
@@ -60,6 +60,8 @@ let result = receiptValidator.validateReceipt {
|
||||
]
|
||||
}
|
||||
|
||||
let result = LocalReceiptValidator().validate(parameters: parameters)
|
||||
|
||||
switch result {
|
||||
case .success(let receipt):
|
||||
print("receipt validated and parsed: \(receipt)")
|
||||
|
||||
Reference in New Issue
Block a user