Compare commits

..

19 Commits

Author SHA1 Message Date
Hannes Oud 60f1b27ec4 Deactivate maccatalyst (as long as it’s not figured out how to compile) 2019-08-27 17:07:57 +02:00
Hannes Oud 6f15dda97a Remove unnecacessary swiftlint disabling 2019-08-27 17:00:02 +02:00
Hannes Oud f7c3e26500 Add dynamic ReceiptOrigin, useful for tests 2019-08-27 16:59:29 +02:00
Hannes Oud 61794a63e3 Fix swift 5.0 badge in readme 2019-05-31 15:10:24 +02:00
Hannes Oud 137a767294 Merge pull request #53 from IdeasOnCanvas/enhancement/intParsingAndDescriptions
Fix quantity and webOrderLineItemID parsing, update for Xcode 10.2, improve convenience
2019-05-31 13:29:13 +02:00
Hannes Oud 11b8faf96c Fix swiftlint warnings 2019-05-31 13:28:24 +02:00
Hannes Oud ee521f9429 Switch to Int64 for ASN1 integer fields, as webOrderLineItemID requires this
# Conflicts:
#	AppReceiptValidator/AppReceiptValidator/OpenSSL/ASN1Helpers.swift
#	AppReceiptValidator/AppReceiptValidator/Receipt.swift
2019-05-31 13:18:57 +02:00
Hannes Oud bf7010545b Update readme for Swift 5.0 2019-05-31 10:32:28 +02:00
Hannes Oud 6a1b919093 Update travis file for Xcode 10.2 2019-05-31 10:29:51 +02:00
Hannes Oud e8146f7122 Disable an unnecessary swiftlint warning 2019-05-23 17:28:30 +02:00
Hannes Oud 2f54c5b042 Update macOS project for Xcode 10.2 / Swift 5 2019-05-23 17:26:16 +02:00
Hannes Oud f0ba73b45e Modernize Data initializer to fix deprectation warning 2019-05-23 17:25:56 +02:00
Hannes Oud 0ff100054d Add Stringy convenience initializers to Receipt and InAppPurchaseReceipt 2019-05-23 17:25:21 +02:00
Hannes Oud 15e3bb5471 Fix test-data after fixing quantity and webOrderLineItemId parsing 2019-05-23 16:47:03 +02:00
Hannes Oud c42f8d9ac9 Fix deprecation warnings using data.withUnsafeBytes 2019-05-23 14:32:07 +02:00
Hannes Oud 0efe3dd839 Update for Xcode 10.2, Swift 5 2019-05-23 14:31:47 +02:00
Hannes Oud cb4690217b Add quotes around strings and dates when creating description 2019-05-23 14:15:23 +02:00
Hannes Oud 8266af7edb Assert octet string when unwrapping and add comments 2019-05-23 14:15:06 +02:00
Hannes Oud 51d190ece0 Unwrap int values correctly (quantity, web order line item) 2019-05-23 14:14:45 +02:00
13 changed files with 166 additions and 77 deletions
+2 -2
View File
@@ -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)
@@ -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;
@@ -1754,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 = "";
@@ -1786,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 = "";
@@ -1819,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 = "";
};
@@ -1851,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 = "";
};
@@ -1869,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;
@@ -1885,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
}
@@ -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 transactions 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 transactions `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
}
+1 -1
View File
@@ -2,7 +2,7 @@
[![Carthage Compatible](https://img.shields.io/badge/Carthage-compatible-4BC51D.svg?style=flat)](https://github.com/Carthage/Carthage)
![Platforms iOS, macOS](https://img.shields.io/badge/Platform-iOS%20|%20macOS-blue.svg "Platforms iOS, macOS")
![Language Swift](https://img.shields.io/badge/Language-Swift%204.2-orange.svg "Swift 4.2")
![Language Swift](https://img.shields.io/badge/Language-Swift%205.0-orange.svg "Swift 5.0")
[![License Apache 2.0 + OpenSSL](https://img.shields.io/badge/License-Apache%202.0%20|%20OpenSSL%20-aaaaff.svg "License")](LICENSE)
[![Build Status](https://travis-ci.org/IdeasOnCanvas/AppReceiptValidator.svg?branch=master)](https://travis-ci.org/IdeasOnCanvas/AppReceiptValidator)
[![Twitter: @hannesoid](https://img.shields.io/badge/Twitter-@hannesoid-red.svg?style=flat)](https://twitter.com/hannesoid)