Compare commits
21 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 60f1b27ec4 | |||
| 6f15dda97a | |||
| f7c3e26500 | |||
| 61794a63e3 | |||
| 137a767294 | |||
| 11b8faf96c | |||
| ee521f9429 | |||
| bf7010545b | |||
| 6a1b919093 | |||
| e8146f7122 | |||
| 2f54c5b042 | |||
| f0ba73b45e | |||
| 0ff100054d | |||
| 15e3bb5471 | |||
| c42f8d9ac9 | |||
| 0efe3dd839 | |||
| cb4690217b | |||
| 8266af7edb | |||
| 51d190ece0 | |||
| 26385ee11b | |||
| a96f16969d |
+2
-2
@@ -1,5 +1,5 @@
|
||||
language: objective-c
|
||||
osx_image: xcode10
|
||||
osx_image: xcode10.2
|
||||
|
||||
env:
|
||||
global:
|
||||
@@ -7,7 +7,7 @@ env:
|
||||
- LANG=en_US.UTF-8
|
||||
- PROJECT=AppReceiptValidator/AppReceiptValidator.xcodeproj
|
||||
matrix:
|
||||
- DESTINATION="OS=12.0,name=iPhone X" SCHEME="AppReceiptValidator Demo iOS"
|
||||
- DESTINATION="OS=12.2,name=iPhone X" SCHEME="AppReceiptValidator Demo iOS"
|
||||
- DESTINATION="arch=x86_64" SCHEME="AppReceiptValidator Demo macOS"
|
||||
|
||||
script:
|
||||
|
||||
@@ -9,9 +9,8 @@
|
||||
import AppReceiptValidator
|
||||
import UIKit
|
||||
|
||||
|
||||
// MARK: - ViewController
|
||||
|
||||
/// Displays two textfields. One to paste a receipt into as base64 string, the other displaying the parsed receipt.
|
||||
/// A device identifier for validation is not supported, have a look at the mac demo instead.
|
||||
class ViewController: UIViewController, UITextViewDelegate {
|
||||
|
||||
@IBOutlet private var inputTextView: UITextView!
|
||||
|
||||
@@ -9,9 +9,8 @@
|
||||
import AppReceiptValidator
|
||||
import Cocoa
|
||||
|
||||
|
||||
// MARK: - ViewController
|
||||
|
||||
/// Displays two textfields. One to paste a receipt into as base64 string, the other displaying the parsed receipt.
|
||||
/// A device identifier can be added in a third field, which is then used to validate the receipt.
|
||||
class ViewController: NSViewController, NSTextViewDelegate, NSTextFieldDelegate {
|
||||
|
||||
private var textFieldObserver: Any?
|
||||
@@ -30,7 +29,7 @@ class ViewController: NSViewController, NSTextViewDelegate, NSTextFieldDelegate
|
||||
self.dropReceivingView.handleDroppedFile = { [unowned self] url in
|
||||
self.update(url: url)
|
||||
}
|
||||
self.textFieldObserver = NotificationCenter.default.addObserver(forName: NSTextField.textDidChangeNotification, object: self.identifierTextField, queue: .main) { [weak self] notification in
|
||||
self.textFieldObserver = NotificationCenter.default.addObserver(forName: NSTextField.textDidChangeNotification, object: self.identifierTextField, queue: .main) { [weak self] _ in
|
||||
guard let self = self else { return }
|
||||
|
||||
self.identifierDidChange(self.identifierTextField)
|
||||
|
||||
+17
-16
@@ -54,7 +54,7 @@ class AppReceiptValidationInAppPurchaseTests: XCTestCase {
|
||||
expirationDate: nil,
|
||||
inAppPurchaseReceipts: [
|
||||
InAppPurchaseReceipt(
|
||||
quantity: nil,
|
||||
quantity: 1,
|
||||
productIdentifier: "consumable",
|
||||
transactionIdentifier: "1000000166865231",
|
||||
originalTransactionIdentifier: "1000000166865231",
|
||||
@@ -62,10 +62,10 @@ class AppReceiptValidationInAppPurchaseTests: XCTestCase {
|
||||
originalPurchaseDate: Date.demoDate(string: "2015-08-07T20:37:55Z"),
|
||||
subscriptionExpirationDate: nil,
|
||||
cancellationDate: nil,
|
||||
webOrderLineItemId: nil
|
||||
webOrderLineItemId: 0
|
||||
),
|
||||
InAppPurchaseReceipt(
|
||||
quantity: nil,
|
||||
quantity: 1,
|
||||
productIdentifier: "monthly",
|
||||
transactionIdentifier: "1000000166965150",
|
||||
originalTransactionIdentifier: "1000000166965150",
|
||||
@@ -73,10 +73,10 @@ class AppReceiptValidationInAppPurchaseTests: XCTestCase {
|
||||
originalPurchaseDate: Date.demoDate(string: "2015-08-10T06:49:33Z"),
|
||||
subscriptionExpirationDate: Date.demoDate(string: "2015-08-10T06:54:32Z"),
|
||||
cancellationDate: nil,
|
||||
webOrderLineItemId: nil
|
||||
webOrderLineItemId: 1000000030274153
|
||||
),
|
||||
InAppPurchaseReceipt( // restores
|
||||
quantity: nil,
|
||||
InAppPurchaseReceipt(
|
||||
quantity: 1,
|
||||
productIdentifier: "monthly",
|
||||
transactionIdentifier: "1000000166965327",
|
||||
originalTransactionIdentifier: "1000000166965150",
|
||||
@@ -84,10 +84,10 @@ class AppReceiptValidationInAppPurchaseTests: XCTestCase {
|
||||
originalPurchaseDate: Date.demoDate(string: "2015-08-10T06:53:18Z"),
|
||||
subscriptionExpirationDate: Date.demoDate(string: "2015-08-10T06:59:32Z"),
|
||||
cancellationDate: nil,
|
||||
webOrderLineItemId: nil
|
||||
webOrderLineItemId: 1000000030274154
|
||||
),
|
||||
InAppPurchaseReceipt(
|
||||
quantity: nil,
|
||||
quantity: 1,
|
||||
productIdentifier: "monthly",
|
||||
transactionIdentifier: "1000000166965895",
|
||||
originalTransactionIdentifier: "1000000166965150",
|
||||
@@ -95,10 +95,10 @@ class AppReceiptValidationInAppPurchaseTests: XCTestCase {
|
||||
originalPurchaseDate: Date.demoDate(string: "2015-08-10T06:57:34Z"),
|
||||
subscriptionExpirationDate: Date.demoDate(string: "2015-08-10T07:04:32Z"),
|
||||
cancellationDate: nil,
|
||||
webOrderLineItemId: nil
|
||||
webOrderLineItemId: 1000000030274165
|
||||
),
|
||||
InAppPurchaseReceipt(
|
||||
quantity: nil,
|
||||
quantity: 1,
|
||||
productIdentifier: "monthly",
|
||||
transactionIdentifier: "1000000166967152",
|
||||
originalTransactionIdentifier: "1000000166965150",
|
||||
@@ -106,10 +106,10 @@ class AppReceiptValidationInAppPurchaseTests: XCTestCase {
|
||||
originalPurchaseDate: Date.demoDate(string: "2015-08-10T07:02:33Z"),
|
||||
subscriptionExpirationDate: Date.demoDate(string: "2015-08-10T07:09:32Z"),
|
||||
cancellationDate: nil,
|
||||
webOrderLineItemId: nil
|
||||
webOrderLineItemId: 1000000030274192
|
||||
),
|
||||
InAppPurchaseReceipt(
|
||||
quantity: nil,
|
||||
quantity: 1,
|
||||
productIdentifier: "monthly",
|
||||
transactionIdentifier: "1000000166967484",
|
||||
originalTransactionIdentifier: "1000000166965150",
|
||||
@@ -117,10 +117,10 @@ class AppReceiptValidationInAppPurchaseTests: XCTestCase {
|
||||
originalPurchaseDate: Date.demoDate(string: "2015-08-10T07:08:30Z"),
|
||||
subscriptionExpirationDate: Date.demoDate(string: "2015-08-10T07:14:32Z"),
|
||||
cancellationDate: nil,
|
||||
webOrderLineItemId: nil
|
||||
webOrderLineItemId: 1000000030274219
|
||||
),
|
||||
InAppPurchaseReceipt(
|
||||
quantity: nil,
|
||||
quantity: 1,
|
||||
productIdentifier: "monthly",
|
||||
transactionIdentifier: "1000000166967782",
|
||||
originalTransactionIdentifier: "1000000166965150",
|
||||
@@ -128,8 +128,9 @@ class AppReceiptValidationInAppPurchaseTests: XCTestCase {
|
||||
originalPurchaseDate: Date.demoDate(string: "2015-08-10T07:12:34Z"),
|
||||
subscriptionExpirationDate: Date.demoDate(string: "2015-08-10T07:19:32Z"),
|
||||
cancellationDate: nil,
|
||||
webOrderLineItemId: nil
|
||||
webOrderLineItemId: 1000000030274249
|
||||
)
|
||||
])
|
||||
]
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -47,8 +47,8 @@ extension KnownOrUnknown: RawRepresentable {
|
||||
|
||||
extension KnownOrUnknown: Hashable {
|
||||
|
||||
public var hashValue: Int {
|
||||
return self.rawValue.hashValue
|
||||
public func hash(into hasher: inout Hasher) {
|
||||
hasher.combine(self.rawValue.hashValue)
|
||||
}
|
||||
|
||||
public static func == (lhs: KnownOrUnknown<Known>, rhs: KnownOrUnknown<Known>) -> Bool {
|
||||
|
||||
@@ -1213,7 +1213,7 @@
|
||||
TargetAttributes = {
|
||||
D19095801F6000A40095729B = {
|
||||
CreatedOnToolsVersion = 9.0;
|
||||
LastSwiftMigration = 1000;
|
||||
LastSwiftMigration = 1020;
|
||||
ProvisioningStyle = Automatic;
|
||||
SystemCapabilities = {
|
||||
com.apple.Sandbox = {
|
||||
@@ -1223,27 +1223,27 @@
|
||||
};
|
||||
D19095911F6000A40095729B = {
|
||||
CreatedOnToolsVersion = 9.0;
|
||||
LastSwiftMigration = 1000;
|
||||
LastSwiftMigration = 1020;
|
||||
ProvisioningStyle = Automatic;
|
||||
};
|
||||
D19095A81F6001800095729B = {
|
||||
CreatedOnToolsVersion = 9.0;
|
||||
LastSwiftMigration = 1000;
|
||||
LastSwiftMigration = 1020;
|
||||
ProvisioningStyle = Automatic;
|
||||
};
|
||||
D1D6F4B41F5D684C00E86FE1 = {
|
||||
CreatedOnToolsVersion = 9.0;
|
||||
LastSwiftMigration = 1000;
|
||||
LastSwiftMigration = 1020;
|
||||
ProvisioningStyle = Automatic;
|
||||
};
|
||||
D1D6F4C11F5D687400E86FE1 = {
|
||||
CreatedOnToolsVersion = 9.0;
|
||||
LastSwiftMigration = 1000;
|
||||
LastSwiftMigration = 1020;
|
||||
ProvisioningStyle = Automatic;
|
||||
};
|
||||
D1D6F4E31F5D691400E86FE1 = {
|
||||
CreatedOnToolsVersion = 9.0;
|
||||
LastSwiftMigration = 1000;
|
||||
LastSwiftMigration = 1020;
|
||||
ProvisioningStyle = Automatic;
|
||||
};
|
||||
};
|
||||
@@ -1529,7 +1529,7 @@
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||
SDKROOT = macosx;
|
||||
SWIFT_VERSION = 4.2;
|
||||
SWIFT_VERSION = 5.0;
|
||||
};
|
||||
name = Debug;
|
||||
};
|
||||
@@ -1547,7 +1547,7 @@
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||
SDKROOT = macosx;
|
||||
SWIFT_VERSION = 4.2;
|
||||
SWIFT_VERSION = 5.0;
|
||||
};
|
||||
name = Release;
|
||||
};
|
||||
@@ -1565,7 +1565,7 @@
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "com.ideasoncanvas.AppReceiptValidator-Demo-macOSTests";
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SDKROOT = macosx;
|
||||
SWIFT_VERSION = 4.2;
|
||||
SWIFT_VERSION = 5.0;
|
||||
};
|
||||
name = Debug;
|
||||
};
|
||||
@@ -1583,7 +1583,7 @@
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "com.ideasoncanvas.AppReceiptValidator-Demo-macOSTests";
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SDKROOT = macosx;
|
||||
SWIFT_VERSION = 4.2;
|
||||
SWIFT_VERSION = 5.0;
|
||||
};
|
||||
name = Release;
|
||||
};
|
||||
@@ -1596,7 +1596,7 @@
|
||||
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks";
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "com.ideasoncanvas.AppReceiptValidator-iOSTests";
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_VERSION = 4.2;
|
||||
SWIFT_VERSION = 5.0;
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
};
|
||||
name = Debug;
|
||||
@@ -1610,7 +1610,7 @@
|
||||
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks";
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "com.ideasoncanvas.AppReceiptValidator-iOSTests";
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_VERSION = 4.2;
|
||||
SWIFT_VERSION = 5.0;
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
};
|
||||
name = Release;
|
||||
@@ -1733,6 +1733,7 @@
|
||||
D1D6F4BB1F5D684C00E86FE1 /* Debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
APPLICATION_EXTENSION_API_ONLY = YES;
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CODE_SIGN_IDENTITY = "";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
@@ -1753,8 +1754,9 @@
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.ideasoncanvas.AppReceiptValidator;
|
||||
PRODUCT_NAME = AppReceiptValidator;
|
||||
SKIP_INSTALL = YES;
|
||||
SUPPORTS_MACCATALYST = NO;
|
||||
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
||||
SWIFT_VERSION = 4.2;
|
||||
SWIFT_VERSION = 5.0;
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
VERSIONING_SYSTEM = "apple-generic";
|
||||
VERSION_INFO_PREFIX = "";
|
||||
@@ -1764,6 +1766,7 @@
|
||||
D1D6F4BC1F5D684C00E86FE1 /* Release */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
APPLICATION_EXTENSION_API_ONLY = YES;
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CODE_SIGN_IDENTITY = "";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
@@ -1784,7 +1787,8 @@
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.ideasoncanvas.AppReceiptValidator;
|
||||
PRODUCT_NAME = AppReceiptValidator;
|
||||
SKIP_INSTALL = YES;
|
||||
SWIFT_VERSION = 4.2;
|
||||
SUPPORTS_MACCATALYST = NO;
|
||||
SWIFT_VERSION = 5.0;
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
VERSIONING_SYSTEM = "apple-generic";
|
||||
VERSION_INFO_PREFIX = "";
|
||||
@@ -1794,6 +1798,7 @@
|
||||
D1D6F4C81F5D687400E86FE1 /* Debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
APPLICATION_EXTENSION_API_ONLY = YES;
|
||||
CODE_SIGN_IDENTITY = "";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
COMBINE_HIDPI_IMAGES = YES;
|
||||
@@ -1816,7 +1821,7 @@
|
||||
PRODUCT_NAME = AppReceiptValidator;
|
||||
SDKROOT = macosx;
|
||||
SKIP_INSTALL = YES;
|
||||
SWIFT_VERSION = 4.2;
|
||||
SWIFT_VERSION = 5.0;
|
||||
VERSIONING_SYSTEM = "apple-generic";
|
||||
VERSION_INFO_PREFIX = "";
|
||||
};
|
||||
@@ -1825,6 +1830,7 @@
|
||||
D1D6F4C91F5D687400E86FE1 /* Release */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
APPLICATION_EXTENSION_API_ONLY = YES;
|
||||
CODE_SIGN_IDENTITY = "";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
COMBINE_HIDPI_IMAGES = YES;
|
||||
@@ -1847,7 +1853,7 @@
|
||||
PRODUCT_NAME = AppReceiptValidator;
|
||||
SDKROOT = macosx;
|
||||
SKIP_INSTALL = YES;
|
||||
SWIFT_VERSION = 4.2;
|
||||
SWIFT_VERSION = 5.0;
|
||||
VERSIONING_SYSTEM = "apple-generic";
|
||||
VERSION_INFO_PREFIX = "";
|
||||
};
|
||||
@@ -1865,7 +1871,7 @@
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "com.ideasoncanvas.AppReceiptValidator.Demo-iOS";
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
||||
SWIFT_VERSION = 4.2;
|
||||
SWIFT_VERSION = 5.0;
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
};
|
||||
name = Debug;
|
||||
@@ -1881,7 +1887,7 @@
|
||||
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "com.ideasoncanvas.AppReceiptValidator.Demo-iOS";
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_VERSION = 4.2;
|
||||
SWIFT_VERSION = 5.0;
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
};
|
||||
name = Release;
|
||||
|
||||
@@ -12,7 +12,7 @@ public extension AppReceiptValidator {
|
||||
|
||||
/// Describes how to validate a receipt, and how/where to obtain the dependencies (receipt, deviceIdentifier, apple root certificate)
|
||||
/// Use .default to initialize the standard parameters. By default, no `propertyValidations` are active.
|
||||
public struct Parameters {
|
||||
struct Parameters {
|
||||
|
||||
// MARK: - Properties
|
||||
|
||||
@@ -62,11 +62,14 @@ extension AppReceiptValidator.Parameters {
|
||||
|
||||
case installedInMainBundle
|
||||
case data(Data)
|
||||
case dynamic(() -> Data?)
|
||||
|
||||
public func loadData() -> Data? {
|
||||
switch self {
|
||||
case .data(let data):
|
||||
return data
|
||||
case .dynamic(let maker):
|
||||
return maker()
|
||||
case .installedInMainBundle:
|
||||
guard let receiptUrl = Bundle.main.appStoreReceiptURL else { return nil }
|
||||
guard (try? receiptUrl.checkResourceIsReachable()) ?? false else { return nil }
|
||||
@@ -85,7 +88,7 @@ public extension AppReceiptValidator.Parameters {
|
||||
///
|
||||
/// - currentDevice: Obtains it from the system location: MAC Address on macOS, deviceIdentifierForVendor on iOS
|
||||
/// - data: Specific Data to use
|
||||
public enum DeviceIdentifier {
|
||||
enum DeviceIdentifier {
|
||||
|
||||
case currentDevice
|
||||
case data(Data)
|
||||
|
||||
@@ -117,15 +117,17 @@ private extension AppReceiptValidator {
|
||||
var sha1Context = SHA_CTX()
|
||||
|
||||
SHA1_Init(&sha1Context)
|
||||
|
||||
deviceIdentifierData.withUnsafeBytes { pointer -> Void in
|
||||
SHA1_Update(&sha1Context, pointer, deviceIdentifierData.count)
|
||||
deviceIdentifierData.withUnsafeBytes { poi -> Void in
|
||||
print(poi)
|
||||
}
|
||||
receiptOpaqueValueData.withUnsafeBytes { pointer -> Void in
|
||||
SHA1_Update(&sha1Context, pointer, receiptOpaqueValueData.count)
|
||||
_ = deviceIdentifierData.withUnsafeBytes { pointer -> Void in
|
||||
SHA1_Update(&sha1Context, pointer.baseAddress, deviceIdentifierData.count)
|
||||
}
|
||||
receiptBundleIdData.withUnsafeBytes { pointer -> Void in
|
||||
SHA1_Update(&sha1Context, pointer, receiptBundleIdData.count)
|
||||
_ = receiptOpaqueValueData.withUnsafeBytes { pointer -> Void in
|
||||
SHA1_Update(&sha1Context, pointer.baseAddress, receiptOpaqueValueData.count)
|
||||
}
|
||||
_ = receiptBundleIdData.withUnsafeBytes { pointer -> Void in
|
||||
SHA1_Update(&sha1Context, pointer.baseAddress, receiptBundleIdData.count)
|
||||
}
|
||||
SHA1_Final(&computedHash, &sha1Context)
|
||||
|
||||
@@ -243,28 +245,30 @@ private extension AppReceiptValidator {
|
||||
return (receipt: receipt, unofficialReceipt: unofficialReceipt)
|
||||
}
|
||||
|
||||
// swiftlint:disable:next cyclomatic_complexity
|
||||
private func parseInAppPurchaseReceipt(pointer: UnsafePointer<UInt8>, length: Int) throws -> InAppPurchaseReceipt {
|
||||
var inAppPurchaseReceipt = InAppPurchaseReceipt()
|
||||
try self.parseASN1Set(pointer: pointer, length: length) { attributeType, value in
|
||||
guard let attribute = KnownInAppPurchaseAttribute(rawValue: attributeType) else { return }
|
||||
guard let value = value.unwrapped else { return } // always unwrap set members
|
||||
|
||||
switch attribute {
|
||||
case .quantity:
|
||||
inAppPurchaseReceipt.quantity = value.intValue
|
||||
case .productIdentifier:
|
||||
inAppPurchaseReceipt.productIdentifier = value.unwrappedStringValue
|
||||
inAppPurchaseReceipt.productIdentifier = value.stringValue
|
||||
case .transactionIdentifier:
|
||||
inAppPurchaseReceipt.transactionIdentifier = value.unwrappedStringValue
|
||||
inAppPurchaseReceipt.transactionIdentifier = value.stringValue
|
||||
case .originalTransactionIdentifier:
|
||||
inAppPurchaseReceipt.originalTransactionIdentifier = value.unwrappedStringValue
|
||||
inAppPurchaseReceipt.originalTransactionIdentifier = value.stringValue
|
||||
case .purchaseDate:
|
||||
inAppPurchaseReceipt.purchaseDate = value.unwrappedDateValue
|
||||
inAppPurchaseReceipt.purchaseDate = value.dateValue
|
||||
case .originalPurchaseDate:
|
||||
inAppPurchaseReceipt.originalPurchaseDate = value.unwrappedDateValue
|
||||
inAppPurchaseReceipt.originalPurchaseDate = value.dateValue
|
||||
case .subscriptionExpirationDate:
|
||||
inAppPurchaseReceipt.subscriptionExpirationDate = value.unwrappedDateValue
|
||||
inAppPurchaseReceipt.subscriptionExpirationDate = value.dateValue
|
||||
case .cancellationDate:
|
||||
inAppPurchaseReceipt.cancellationDate = value.unwrappedDateValue
|
||||
inAppPurchaseReceipt.cancellationDate = value.dateValue
|
||||
case .webOrderLineItemId:
|
||||
inAppPurchaseReceipt.webOrderLineItemId = value.intValue
|
||||
}
|
||||
|
||||
+1
-1
@@ -53,6 +53,6 @@ extension AppReceiptValidator.Parameters.DeviceIdentifier {
|
||||
.map { String(format: "%02x", $0) }
|
||||
.joined(separator: ":")
|
||||
|
||||
return (data: Data(bytes: address), addressString: addressString)
|
||||
return (data: Data(address), addressString: addressString)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,6 +9,8 @@
|
||||
import AppReceiptValidator.OpenSSL
|
||||
import Foundation
|
||||
|
||||
// A resource for ASN1 can be https://www.oss.com/asn1/resources/reference/asn1-reference-card.html
|
||||
|
||||
/// An ASN1 Sequence Object. Of interest are the attributeType and the valueObject.
|
||||
/// The attributeType determines how to interpret the valueObject.
|
||||
///
|
||||
@@ -116,7 +118,11 @@ extension ASN1Object {
|
||||
|
||||
extension ASN1Object {
|
||||
|
||||
/// Unwraps an OCTET_STRING, which is a binary container, that can contain another ASN1Object.
|
||||
/// This is the case when we are looking at an entry in an ASN1Set
|
||||
var unwrapped: ASN1Object? {
|
||||
guard self.type == V_ASN1_OCTET_STRING else { return nil }
|
||||
|
||||
guard let endPointer = valuePointer?.advanced(by: length) else { return nil }
|
||||
|
||||
var innerPointer = valuePointer
|
||||
@@ -155,7 +161,11 @@ extension ASN1Object {
|
||||
|
||||
extension ASN1Object {
|
||||
|
||||
var intValue: Int? {
|
||||
var unwrappedIntValue: Int64? {
|
||||
return self.unwrapped?.intValue
|
||||
}
|
||||
|
||||
var intValue: Int64? {
|
||||
guard self.type == V_ASN1_INTEGER else { return nil }
|
||||
|
||||
var pointer = self.valuePointer
|
||||
@@ -163,11 +173,11 @@ extension ASN1Object {
|
||||
defer {
|
||||
ASN1_INTEGER_free(integer)
|
||||
}
|
||||
let result = ASN1_INTEGER_get(integer)
|
||||
let result = Int64(ASN1_INTEGER_get(integer))
|
||||
return result
|
||||
}
|
||||
|
||||
func intValue(byAdvancingPointer pointer: inout UnsafePointer<UInt8>?, length: Int? = nil) -> Int? {
|
||||
func intValue(byAdvancingPointer pointer: inout UnsafePointer<UInt8>?, length: Int? = nil) -> Int64? {
|
||||
let length = length ?? self.length
|
||||
pointer = pointer?.advanced(by: length)
|
||||
guard let intValue = self.intValue else { return nil }
|
||||
|
||||
@@ -14,8 +14,8 @@ final class BIOWrapper {
|
||||
let bio = BIO_new(BIO_s_mem())
|
||||
|
||||
init(data: Data) {
|
||||
data.withUnsafeBytes { pointer -> Void in
|
||||
BIO_write(self.bio, pointer, Int32(data.count))
|
||||
_ = data.withUnsafeBytes { pointer in
|
||||
BIO_write(self.bio, pointer.baseAddress, Int32(data.count))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -55,6 +55,7 @@ public struct Receipt: Equatable {
|
||||
/// The in-app purchase receipt for a non-consumable product, auto-renewable subscription, non-renewing subscription, or free subscription remains in the receipt indefinitely.
|
||||
public internal(set) var inAppPurchaseReceipts: [InAppPurchaseReceipt] = []
|
||||
|
||||
/// For documentation of parameters, have a look at the documented properties of `Receipt`
|
||||
public init(bundleIdentifier: String?, bundleIdData: Data?, appVersion: String?, opaqueValue: Data?, sha1Hash: Data?, originalAppVersion: String?, receiptCreationDate: Date?, expirationDate: Date?, inAppPurchaseReceipts: [InAppPurchaseReceipt]) {
|
||||
self.bundleIdentifier = bundleIdentifier
|
||||
self.bundleIdData = bundleIdData
|
||||
@@ -67,6 +68,27 @@ public struct Receipt: Equatable {
|
||||
self.inAppPurchaseReceipts = inAppPurchaseReceipts
|
||||
}
|
||||
|
||||
/// Convenience initializer with stringified parameters for `Date` and `Data` type parameters.
|
||||
/// When logging a `Receipt`'s (`CustomStringConvertible`) description, you can use that as swift source code calling this initializer,
|
||||
/// which can simplify creating tests.
|
||||
///
|
||||
/// For documentation of parameters, have a look at the documented properties of `Receipt`.
|
||||
///
|
||||
/// - Note: Dates must be formatted as "2017-01-01T12:00:00Z", otherwise will be assigned `nil`.
|
||||
/// Data must be formatted as Base64, otherwise will be assigned `nil`.
|
||||
/// Correct formatting will be asserted in DEBUG builds.
|
||||
public init(bundleIdentifier: String, bundleIdData: String, appVersion: String, opaqueValue: String, sha1Hash: String, originalAppVersion: String, receiptCreationDate: String, expirationDate: String?, inAppPurchaseReceipts: [InAppPurchaseReceipt]) {
|
||||
self.init(bundleIdentifier: bundleIdentifier,
|
||||
bundleIdData: parseBase64(string: bundleIdData),
|
||||
appVersion: appVersion,
|
||||
opaqueValue: parseBase64(string: opaqueValue),
|
||||
sha1Hash: parseBase64(string: sha1Hash),
|
||||
originalAppVersion: originalAppVersion,
|
||||
receiptCreationDate: parseDate(string: receiptCreationDate),
|
||||
expirationDate: expirationDate.flatMap { parseDate(string: $0) },
|
||||
inAppPurchaseReceipts: inAppPurchaseReceipts)
|
||||
}
|
||||
|
||||
public init() {}
|
||||
}
|
||||
|
||||
@@ -74,6 +96,8 @@ public struct Receipt: Equatable {
|
||||
|
||||
extension Receipt: CustomStringConvertible, CustomDebugStringConvertible {
|
||||
|
||||
// This description is carefully matched to match the stringy convenience initializer of `Receipt`
|
||||
|
||||
public var description: String {
|
||||
let formatter = StringFormatter()
|
||||
let props: [(String, String)] = [
|
||||
@@ -101,7 +125,7 @@ extension Receipt: CustomStringConvertible, CustomDebugStringConvertible {
|
||||
///
|
||||
/// Documentation was obtained from: https://developer.apple.com/library/content/releasenotes/General/ValidateAppStoreReceipt/Chapters/ReceiptFields.html
|
||||
///
|
||||
/// The following fields are part of JSON communication but not part of the parsed version (matched Sept 2017):
|
||||
/// The following fields are part of JSON communication but not part of the parsed version (matched May 2019):
|
||||
/// - Subscription Expiration Intent
|
||||
/// - Subscription Retry Flag
|
||||
/// - Subscription Trial Period
|
||||
@@ -115,7 +139,7 @@ public struct InAppPurchaseReceipt: Equatable {
|
||||
|
||||
/// The number of items purchased. ASN.1 Field Type 1701.
|
||||
/// This value corresponds to the quantity property of the `SKPayment` object stored in the transaction’s payment property.
|
||||
public internal(set) var quantity: Int?
|
||||
public internal(set) var quantity: Int64?
|
||||
|
||||
/// The product identifier of the item that was purchased. ASN.1 Field Type 1702.
|
||||
/// This value corresponds to the `productIdentifier` property of the `SKPayment` object stored in the transaction’s `payment` property.
|
||||
@@ -167,10 +191,10 @@ public struct InAppPurchaseReceipt: Equatable {
|
||||
|
||||
/// The primary key for identifying subscription purchases. ASN.1 Field Type 1711.
|
||||
/// This value is a unique ID that identifies purchase events across devices, including subscription renewal purchase events.
|
||||
public internal(set) var webOrderLineItemId: Int?
|
||||
public internal(set) var webOrderLineItemId: Int64?
|
||||
|
||||
/// 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?) {
|
||||
/// For documentation of parameters, have a look at the documented properties of `InAppPurchaseReceipt`.
|
||||
public init(quantity: Int64?, productIdentifier: String?, transactionIdentifier: String?, originalTransactionIdentifier: String?, purchaseDate: Date?, originalPurchaseDate: Date?, subscriptionExpirationDate: Date?, cancellationDate: Date?, webOrderLineItemId: Int64?) {
|
||||
self.quantity = quantity
|
||||
self.productIdentifier = productIdentifier
|
||||
self.transactionIdentifier = transactionIdentifier
|
||||
@@ -182,6 +206,27 @@ public struct InAppPurchaseReceipt: Equatable {
|
||||
self.webOrderLineItemId = webOrderLineItemId
|
||||
}
|
||||
|
||||
/// Convenience initializer with stringified parameters for `Date` and `Data` type parameters.
|
||||
/// When logging a `Receipt`'s (`CustomStringConvertible`) description, you can use that as swift source code calling this initializer,
|
||||
/// which can simplify creating tests.
|
||||
///
|
||||
/// For documentation of parameters, have a look at the documented properties of `InAppPurchaseReceipt`.
|
||||
///
|
||||
/// - Note: Dates must be formatted as "2017-01-01T12:00:00Z", otherwise will be assigned `nil`.
|
||||
/// Data must be formatted as Base64, otherwise will be assigned `nil`.
|
||||
/// Correct formatting will be asserted in DEBUG builds.
|
||||
public init(quantity: Int64?, productIdentifier: String, transactionIdentifier: String, originalTransactionIdentifier: String, purchaseDate: String, originalPurchaseDate: String, subscriptionExpirationDate: String?, cancellationDate: String?, webOrderLineItemId: Int64?) {
|
||||
self.init(quantity: quantity,
|
||||
productIdentifier: productIdentifier,
|
||||
transactionIdentifier: transactionIdentifier,
|
||||
originalTransactionIdentifier: originalTransactionIdentifier,
|
||||
purchaseDate: parseDate(string: purchaseDate),
|
||||
originalPurchaseDate: parseDate(string: originalPurchaseDate),
|
||||
subscriptionExpirationDate: subscriptionExpirationDate.flatMap { parseDate(string: $0) },
|
||||
cancellationDate: cancellationDate.flatMap { parseDate(string: $0) },
|
||||
webOrderLineItemId: webOrderLineItemId)
|
||||
}
|
||||
|
||||
public init() {}
|
||||
}
|
||||
|
||||
@@ -189,6 +234,8 @@ public struct InAppPurchaseReceipt: Equatable {
|
||||
|
||||
extension InAppPurchaseReceipt: CustomStringConvertible, CustomDebugStringConvertible {
|
||||
|
||||
// This description is carefully matched to match the stringy convenience initializer of `InAppPurchaseReceipt`
|
||||
|
||||
public var description: String {
|
||||
let formatter = StringFormatter()
|
||||
let props: [(String, String)] = [
|
||||
@@ -228,7 +275,7 @@ private struct StringFormatter {
|
||||
return pairs.map { self.format(key: $0, value: $1) }.joined(separator: ",\n")
|
||||
}
|
||||
|
||||
func format(_ int: Int?) -> String {
|
||||
func format(_ int: Int64?) -> String {
|
||||
guard let int = int else { return fallback }
|
||||
|
||||
return "\(int)"
|
||||
@@ -241,16 +288,40 @@ private struct StringFormatter {
|
||||
func format(_ data: Data?) -> String {
|
||||
guard let data = data else { return fallback }
|
||||
|
||||
return data.base64EncodedString()
|
||||
return quoted(data.base64EncodedString())
|
||||
}
|
||||
|
||||
func format(_ date: Date?) -> String {
|
||||
guard let date = date else { return fallback }
|
||||
|
||||
return AppReceiptValidator.asn1DateFormatter.string(from: date)
|
||||
return quoted(AppReceiptValidator.asn1DateFormatter.string(from: date))
|
||||
}
|
||||
|
||||
func format(_ string: String?) -> String {
|
||||
return string ?? fallback
|
||||
return string.map(quoted) ?? fallback
|
||||
}
|
||||
|
||||
/// Surrounds a string with quotes "…"
|
||||
func quoted(_ string: String) -> String {
|
||||
return "\"" + string + "\""
|
||||
}
|
||||
}
|
||||
|
||||
private func parseBase64(string: String) -> Data? {
|
||||
guard let data = Data(base64Encoded: string) else {
|
||||
assertionFailure("Data could not be parsed from string '\(string)', make sure it is non-empty nad base64 encoded.")
|
||||
return nil
|
||||
}
|
||||
|
||||
return data
|
||||
}
|
||||
|
||||
// Parses a string of type "2017-01-01T12:00:00Z"
|
||||
private func parseDate(string: String) -> Date? {
|
||||
guard let date = AppReceiptValidator.asn1DateFormatter.date(from: string) else {
|
||||
assertionFailure("Date could not be parsed from string '\(string)', make sure it has a correct format, example `2017-01-01T12:00:00Z`")
|
||||
return nil
|
||||
}
|
||||
|
||||
return date
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
[](https://github.com/Carthage/Carthage)
|
||||

|
||||

|
||||

|
||||
[](LICENSE)
|
||||
[](https://travis-ci.org/IdeasOnCanvas/AppReceiptValidator)
|
||||
[](https://twitter.com/hannesoid)
|
||||
|
||||
Reference in New Issue
Block a user