Compare commits

...

3 Commits

Author SHA1 Message Date
Hannes Oud 0a0966de40 Add bear subscription initial offer test receipt 2019-09-26 16:50:01 +02:00
Hannes Oud 2edf4062c0 Add isInIntroductoryOfferPeriod to transaction and parse it 2019-09-26 16:49:27 +02:00
Michael Schwarz 29f22e4c7d Merge pull request #56 from IdeasOnCanvas/enhancement/dynamicData
Add dynamic data retriever as Receipt Origin
2019-08-27 17:03:25 +02:00
5 changed files with 80 additions and 11 deletions
@@ -13,6 +13,46 @@ class AppReceiptValidationInAppPurchaseTests: XCTestCase {
var receiptValidator = AppReceiptValidator()
func testBearInitialSubscriptionReceiptParsing() {
guard let data = assertTestAsset(filename: "hannes_bear_introductory_1_week_trial_receipt") else { return }
// A monthly subscription to Bear on macOS with 2 week introductory offer (free trial) just started.
let expected = Receipt(
bundleIdentifier: "net.shinyfrog.bear",
bundleIdData: "DBJuZXQuc2hpbnlmcm9nLmJlYXI=",
appVersion: "1.7.3",
opaqueValue: "9MIfR4OoEFSOXc1fQ1ryDA==",
sha1Hash: "fgStBECp2Yi0PONokIjcbBpnhvM=",
originalAppVersion: "1.7.3",
receiptCreationDate: "2019-09-26T14:08:37Z",
expirationDate: nil,
inAppPurchaseReceipts: [
InAppPurchaseReceipt(
quantity: 1,
productIdentifier: "net.shinyfrog.bear.pro_monthly_subscription",
transactionIdentifier: "240000651265628",
originalTransactionIdentifier: "240000651265628",
purchaseDate: "2019-09-26T14:08:36Z",
originalPurchaseDate: "2019-09-26T14:08:36Z",
subscriptionExpirationDate: "2019-10-03T14:08:36Z",
cancellationDate: nil,
webOrderLineItemId: 240000225193522,
isInIntroductoryPricePeriod: false // should this not be true?
)
]
)
let result = receiptValidator.validateReceipt {
$0.receiptOrigin = .data(data)
$0.shouldValidateHash = false // the original device identifier is unknown
}
guard let receipt = result.receipt else {
XCTFail("Unexpectedly failed parsing a receipt \(result.error!)")
return
}
XCTAssertEqual(receipt, expected)
}
func testNonMindNodeReceiptParsingWithoutValidation() {
guard let data = assertB64TestAsset(filename: "grandUnifiedExpiredAppleCert_receipt.b64") else { return }
@@ -62,7 +102,8 @@ class AppReceiptValidationInAppPurchaseTests: XCTestCase {
originalPurchaseDate: Date.demoDate(string: "2015-08-07T20:37:55Z"),
subscriptionExpirationDate: nil,
cancellationDate: nil,
webOrderLineItemId: 0
webOrderLineItemId: 0,
isInIntroductoryPricePeriod: nil
),
InAppPurchaseReceipt(
quantity: 1,
@@ -73,7 +114,8 @@ class AppReceiptValidationInAppPurchaseTests: XCTestCase {
originalPurchaseDate: Date.demoDate(string: "2015-08-10T06:49:33Z"),
subscriptionExpirationDate: Date.demoDate(string: "2015-08-10T06:54:32Z"),
cancellationDate: nil,
webOrderLineItemId: 1000000030274153
webOrderLineItemId: 1000000030274153,
isInIntroductoryPricePeriod: nil
),
InAppPurchaseReceipt(
quantity: 1,
@@ -84,7 +126,8 @@ class AppReceiptValidationInAppPurchaseTests: XCTestCase {
originalPurchaseDate: Date.demoDate(string: "2015-08-10T06:53:18Z"),
subscriptionExpirationDate: Date.demoDate(string: "2015-08-10T06:59:32Z"),
cancellationDate: nil,
webOrderLineItemId: 1000000030274154
webOrderLineItemId: 1000000030274154,
isInIntroductoryPricePeriod: nil
),
InAppPurchaseReceipt(
quantity: 1,
@@ -95,7 +138,8 @@ class AppReceiptValidationInAppPurchaseTests: XCTestCase {
originalPurchaseDate: Date.demoDate(string: "2015-08-10T06:57:34Z"),
subscriptionExpirationDate: Date.demoDate(string: "2015-08-10T07:04:32Z"),
cancellationDate: nil,
webOrderLineItemId: 1000000030274165
webOrderLineItemId: 1000000030274165,
isInIntroductoryPricePeriod: nil
),
InAppPurchaseReceipt(
quantity: 1,
@@ -106,7 +150,8 @@ class AppReceiptValidationInAppPurchaseTests: XCTestCase {
originalPurchaseDate: Date.demoDate(string: "2015-08-10T07:02:33Z"),
subscriptionExpirationDate: Date.demoDate(string: "2015-08-10T07:09:32Z"),
cancellationDate: nil,
webOrderLineItemId: 1000000030274192
webOrderLineItemId: 1000000030274192,
isInIntroductoryPricePeriod: nil
),
InAppPurchaseReceipt(
quantity: 1,
@@ -117,7 +162,8 @@ class AppReceiptValidationInAppPurchaseTests: XCTestCase {
originalPurchaseDate: Date.demoDate(string: "2015-08-10T07:08:30Z"),
subscriptionExpirationDate: Date.demoDate(string: "2015-08-10T07:14:32Z"),
cancellationDate: nil,
webOrderLineItemId: 1000000030274219
webOrderLineItemId: 1000000030274219,
isInIntroductoryPricePeriod: nil
),
InAppPurchaseReceipt(
quantity: 1,
@@ -128,7 +174,8 @@ class AppReceiptValidationInAppPurchaseTests: XCTestCase {
originalPurchaseDate: Date.demoDate(string: "2015-08-10T07:12:34Z"),
subscriptionExpirationDate: Date.demoDate(string: "2015-08-10T07:19:32Z"),
cancellationDate: nil,
webOrderLineItemId: 1000000030274249
webOrderLineItemId: 1000000030274249,
isInIntroductoryPricePeriod: nil
)
]
)
@@ -31,6 +31,8 @@
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 /* AppReceiptValidator.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = D1D6F4B51F5D684C00E86FE1 /* AppReceiptValidator.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
D1545DDD233D062A00BE82F8 /* hannes_bear_introductory_1_week_trial_receipt in Resources */ = {isa = PBXBuildFile; fileRef = D1545DDC233D062A00BE82F8 /* hannes_bear_introductory_1_week_trial_receipt */; };
D1545DDE233D062A00BE82F8 /* hannes_bear_introductory_1_week_trial_receipt in Resources */ = {isa = PBXBuildFile; fileRef = D1545DDC233D062A00BE82F8 /* hannes_bear_introductory_1_week_trial_receipt */; };
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, ); }; };
@@ -288,6 +290,7 @@
D150A0ED1F669A880026ED04 /* AppReceiptValidationInAppPurchaseTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppReceiptValidationInAppPurchaseTests.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>"; };
D1545DDC233D062A00BE82F8 /* hannes_bear_introductory_1_week_trial_receipt */ = {isa = PBXFileReference; lastKnownFileType = file; path = hannes_bear_introductory_1_week_trial_receipt; sourceTree = "<group>"; };
D15C59141F698005006F66FE /* AppReceiptValidator.modulemap */ = {isa = PBXFileReference; lastKnownFileType = "sourcecode.module-map"; path = AppReceiptValidator.modulemap; sourceTree = "<group>"; };
D19095811F6000A40095729B /* AppReceiptValidator Demo macOS.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "AppReceiptValidator Demo macOS.app"; sourceTree = BUILT_PRODUCTS_DIR; };
D19095831F6000A40095729B /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
@@ -903,6 +906,7 @@
D1D6F5451F5D8DF700E86FE1 /* hannes_mac_mindnode_pro_receipt */,
D150A0E21F66936B0026ED04 /* mindnode_ios_michaelsandbox_receipt1.b64 */,
D150A0E81F6698950026ED04 /* mindnode_ios_michaelsandbox_receipt2.b64 */,
D1545DDC233D062A00BE82F8 /* hannes_bear_introductory_1_week_trial_receipt */,
D1D6F54F1F5D9E8D00E86FE1 /* not_a_receipt */,
);
path = "Test Assets";
@@ -1289,6 +1293,7 @@
D14FA7331F61476800545540 /* mac_mindnode_rebought_receipt in Resources */,
D19095A21F6000DE0095729B /* hannes_mac_mindnode_pro_Info.plist in Resources */,
D19095A11F6000DE0095729B /* hannes_mac_mindnode_receipt in Resources */,
D1545DDE233D062A00BE82F8 /* hannes_bear_introductory_1_week_trial_receipt in Resources */,
D15358AA1F62C12400F297D0 /* grandUnifiedExpiredAppleCert_receipt.b64 in Resources */,
D19095A41F6000DE0095729B /* not_a_receipt in Resources */,
D150A0E91F66989A0026ED04 /* mindnode_ios_michaelsandbox_receipt2.b64 in Resources */,
@@ -1306,6 +1311,7 @@
D14FA7321F61476700545540 /* mac_mindnode_rebought_receipt in Resources */,
D19095CA1F601E5D0095729B /* hannes_mac_mindnode_pro_Info.plist in Resources */,
D19095C91F601E5D0095729B /* hannes_mac_mindnode_receipt in Resources */,
D1545DDD233D062A00BE82F8 /* hannes_bear_introductory_1_week_trial_receipt in Resources */,
D15358AE1F62C12500F297D0 /* grandUnifiedExpiredAppleCert_receipt.b64 in Resources */,
D19095CC1F601E5D0095729B /* not_a_receipt in Resources */,
D150A0EA1F66989A0026ED04 /* mindnode_ios_michaelsandbox_receipt2.b64 in Resources */,
@@ -271,6 +271,8 @@ private extension AppReceiptValidator {
inAppPurchaseReceipt.cancellationDate = value.dateValue
case .webOrderLineItemId:
inAppPurchaseReceipt.webOrderLineItemId = value.intValue
case .isInIntroductoryOfferPeriod:
inAppPurchaseReceipt.isInIntroductoryPricePeriod = value.intValue.flatMap { $0 != 0 }
}
}
return inAppPurchaseReceipt
@@ -351,6 +353,7 @@ private extension AppReceiptValidator {
case subscriptionExpirationDate = 1708
case cancellationDate = 1712
case webOrderLineItemId = 1711
case isInIntroductoryOfferPeriod = 1719
}
}
@@ -193,8 +193,12 @@ public struct InAppPurchaseReceipt: Equatable {
/// This value is a unique ID that identifies purchase events across devices, including subscription renewal purchase events.
public internal(set) var webOrderLineItemId: Int64?
/// For an **auto-renewable subscription**, whether or not it is in the introductory price period. For other transactions this is `nil`
/// ASN.1 Field Type 1719
public internal(set) var isInIntroductoryPricePeriod: Bool?
/// 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?) {
public init(quantity: Int64?, productIdentifier: String?, transactionIdentifier: String?, originalTransactionIdentifier: String?, purchaseDate: Date?, originalPurchaseDate: Date?, subscriptionExpirationDate: Date?, cancellationDate: Date?, webOrderLineItemId: Int64?, isInIntroductoryPricePeriod: Bool?) {
self.quantity = quantity
self.productIdentifier = productIdentifier
self.transactionIdentifier = transactionIdentifier
@@ -204,6 +208,7 @@ public struct InAppPurchaseReceipt: Equatable {
self.subscriptionExpirationDate = subscriptionExpirationDate
self.cancellationDate = cancellationDate
self.webOrderLineItemId = webOrderLineItemId
self.isInIntroductoryPricePeriod = isInIntroductoryPricePeriod
}
/// Convenience initializer with stringified parameters for `Date` and `Data` type parameters.
@@ -215,7 +220,7 @@ public struct InAppPurchaseReceipt: Equatable {
/// - 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?) {
public init(quantity: Int64?, productIdentifier: String, transactionIdentifier: String, originalTransactionIdentifier: String, purchaseDate: String, originalPurchaseDate: String, subscriptionExpirationDate: String?, cancellationDate: String?, webOrderLineItemId: Int64?, isInIntroductoryPricePeriod: Bool?) {
self.init(quantity: quantity,
productIdentifier: productIdentifier,
transactionIdentifier: transactionIdentifier,
@@ -224,7 +229,8 @@ public struct InAppPurchaseReceipt: Equatable {
originalPurchaseDate: parseDate(string: originalPurchaseDate),
subscriptionExpirationDate: subscriptionExpirationDate.flatMap { parseDate(string: $0) },
cancellationDate: cancellationDate.flatMap { parseDate(string: $0) },
webOrderLineItemId: webOrderLineItemId)
webOrderLineItemId: webOrderLineItemId,
isInIntroductoryPricePeriod: isInIntroductoryPricePeriod)
}
public init() {}
@@ -247,7 +253,8 @@ extension InAppPurchaseReceipt: CustomStringConvertible, CustomDebugStringConver
("originalPurchaseDate", formatter.format(self.originalPurchaseDate)),
("subscriptionExpirationDate", formatter.format(self.subscriptionExpirationDate)),
("cancellationDate", formatter.format(self.cancellationDate)),
("webOrderLineItemId", formatter.format(self.webOrderLineItemId))
("webOrderLineItemId", formatter.format(self.webOrderLineItemId)),
("isInIntroductoryPricePeriod", formatter.format(self.isInIntroductoryPricePeriod))
]
return "InAppPurchaseReceipt(\n" + formatter.format(props) + "\n)"
}
@@ -291,6 +298,12 @@ private struct StringFormatter {
return quoted(data.base64EncodedString())
}
func format(_ optionalBool: Bool?) -> String {
guard let bool = optionalBool else { return fallback }
return String(describing: bool)
}
func format(_ date: Date?) -> String {
guard let date = date else { return fallback }