From 3f1d44df9c717c4b0185f6051e46a2159ffe912e Mon Sep 17 00:00:00 2001 From: Andy Boedo Date: Thu, 27 Aug 2020 15:40:35 -0300 Subject: [PATCH 1/6] added basic logic to early exit restoreTransactions if there are no transactions on the receipt --- Purchases/Public/RCPurchases.m | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/Purchases/Public/RCPurchases.m b/Purchases/Public/RCPurchases.m index 7419a531d5..d97798f1aa 100644 --- a/Purchases/Public/RCPurchases.m +++ b/Purchases/Public/RCPurchases.m @@ -625,6 +625,15 @@ - (void)restoreTransactionsWithCompletionBlock:(nullable RCReceivePurchaserInfoB CALL_IF_SET_ON_MAIN_THREAD(completion, nil, [RCPurchasesErrorUtils missingReceiptFileError]); return; } + + RCPurchaserInfo * _Nullable cachedPurchaserInfo = [self readPurchaserInfoFromCache]; + BOOL hasOriginalPurchaseDate = cachedPurchaserInfo != nil && cachedPurchaserInfo.originalPurchaseDate != nil; + BOOL receiptHasTransactions = NO; // TODO + if (!receiptHasTransactions && hasOriginalPurchaseDate) { + CALL_IF_SET_ON_MAIN_THREAD(completion, cachedPurchaserInfo, nil); + return; + } + RCSubscriberAttributeDict subscriberAttributes = self.unsyncedAttributesByKey; [self.backend postReceiptData:data appUserID:self.appUserID From 0c9bc0d1fea26011350393853abdbf08e5a92ef7 Mon Sep 17 00:00:00 2001 From: Andy Boedo Date: Thu, 27 Aug 2020 16:02:11 -0300 Subject: [PATCH 2/6] made ReceiptParser public, added a method to check if a receipt has transactions, used it to decide whether or not to post the receipt --- Purchases/Public/RCPurchases.m | 2 +- .../LocalReceiptParsing/ReceiptParser.swift | 28 ++++++++++++++----- .../ReceiptParserTests.swift | 3 +- .../Mocks/MockReceiptParser.swift | 5 ++++ 4 files changed, 29 insertions(+), 9 deletions(-) diff --git a/Purchases/Public/RCPurchases.m b/Purchases/Public/RCPurchases.m index d97798f1aa..62c4c5fd11 100644 --- a/Purchases/Public/RCPurchases.m +++ b/Purchases/Public/RCPurchases.m @@ -628,7 +628,7 @@ - (void)restoreTransactionsWithCompletionBlock:(nullable RCReceivePurchaserInfoB RCPurchaserInfo * _Nullable cachedPurchaserInfo = [self readPurchaserInfoFromCache]; BOOL hasOriginalPurchaseDate = cachedPurchaserInfo != nil && cachedPurchaserInfo.originalPurchaseDate != nil; - BOOL receiptHasTransactions = NO; // TODO + BOOL receiptHasTransactions = [[[RCReceiptParser alloc] init] receiptHasTransactionsWithReceiptData:data]; if (!receiptHasTransactions && hasOriginalPurchaseDate) { CALL_IF_SET_ON_MAIN_THREAD(completion, cachedPurchaserInfo, nil); return; diff --git a/PurchasesCoreSwift/LocalReceiptParsing/ReceiptParser.swift b/PurchasesCoreSwift/LocalReceiptParsing/ReceiptParser.swift index 5f76271a32..208412e9de 100644 --- a/PurchasesCoreSwift/LocalReceiptParsing/ReceiptParser.swift +++ b/PurchasesCoreSwift/LocalReceiptParsing/ReceiptParser.swift @@ -8,17 +8,31 @@ import Foundation -class ReceiptParser { - private let objectIdentifierParser: ASN1ObjectIdentifierBuilder +@objc(RCReceiptParser) public class ReceiptParser: NSObject { + private let objectIdentifierBuilder: ASN1ObjectIdentifierBuilder private let containerBuilder: ASN1ContainerBuilder private let receiptBuilder: AppleReceiptBuilder - init(objectIdentifierParser: ASN1ObjectIdentifierBuilder = ASN1ObjectIdentifierBuilder(), - containerBuilder: ASN1ContainerBuilder = ASN1ContainerBuilder(), - receiptBuilder: AppleReceiptBuilder = AppleReceiptBuilder()) { - self.objectIdentifierParser = objectIdentifierParser + @objc public convenience override init() { + self.init(objectIdentifierBuilder: ASN1ObjectIdentifierBuilder(), + containerBuilder: ASN1ContainerBuilder(), + receiptBuilder: AppleReceiptBuilder()) + } + + init(objectIdentifierBuilder: ASN1ObjectIdentifierBuilder, + containerBuilder: ASN1ContainerBuilder, + receiptBuilder: AppleReceiptBuilder) { + self.objectIdentifierBuilder = objectIdentifierBuilder self.containerBuilder = containerBuilder self.receiptBuilder = receiptBuilder + super.init() + } + + @objc public func receiptHasTransactions(receiptData: Data) -> Bool { + if let receipt = try? parse(from: receiptData) { + return receipt.inAppPurchases.count > 0 + } + return false } func parse(from receiptData: Data) throws -> AppleReceipt { @@ -40,7 +54,7 @@ private extension ReceiptParser { if container.encodingType == .constructed { for (index, internalContainer) in container.internalContainers.enumerated() { if internalContainer.containerIdentifier == .objectIdentifier { - let objectIdentifier = objectIdentifierParser.build(fromPayload: internalContainer.internalPayload) + let objectIdentifier = objectIdentifierBuilder.build(fromPayload: internalContainer.internalPayload) if objectIdentifier == objectId && index < container.internalContainers.count - 1 { // the container that holds the data comes right after the one with the object identifier return container.internalContainers[index + 1] diff --git a/PurchasesCoreSwiftTests/LocalReceiptParsing/ReceiptParserTests.swift b/PurchasesCoreSwiftTests/LocalReceiptParsing/ReceiptParserTests.swift index 89ddad2ab3..9f84fb0aaa 100644 --- a/PurchasesCoreSwiftTests/LocalReceiptParsing/ReceiptParserTests.swift +++ b/PurchasesCoreSwiftTests/LocalReceiptParsing/ReceiptParserTests.swift @@ -14,7 +14,8 @@ class ReceiptParserTests: XCTestCase { super.setUp() mockAppleReceiptBuilder = MockAppleReceiptBuilder() mockASN1ContainerBuilder = MockASN1ContainerBuilder() - receiptParser = ReceiptParser(containerBuilder: mockASN1ContainerBuilder, + receiptParser = ReceiptParser(objectIdentifierBuilder: ASN1ObjectIdentifierBuilder(), + containerBuilder: mockASN1ContainerBuilder, receiptBuilder: mockAppleReceiptBuilder) } diff --git a/PurchasesCoreSwiftTests/Mocks/MockReceiptParser.swift b/PurchasesCoreSwiftTests/Mocks/MockReceiptParser.swift index 3f6ca200cd..b7e5055220 100644 --- a/PurchasesCoreSwiftTests/Mocks/MockReceiptParser.swift +++ b/PurchasesCoreSwiftTests/Mocks/MockReceiptParser.swift @@ -22,6 +22,11 @@ class MockReceiptParser: ReceiptParser { expirationDate: nil, inAppPurchases: []) + convenience init() { + self.init(objectIdentifierBuilder: ASN1ObjectIdentifierBuilder(), + containerBuilder: MockASN1ContainerBuilder(), + receiptBuilder: MockAppleReceiptBuilder()) + } override func parse(from receiptData: Data) throws -> AppleReceipt { invokedParse = true invokedParseCount += 1 From 7f6d80f8ace327885b8ddfe7f08ad2b65e193516 Mon Sep 17 00:00:00 2001 From: Andy Boedo Date: Thu, 27 Aug 2020 16:33:19 -0300 Subject: [PATCH 3/6] added tests for receiptHasTransactions --- .../ReceiptParserTests.swift | 61 ++++++++++++++++++- 1 file changed, 58 insertions(+), 3 deletions(-) diff --git a/PurchasesCoreSwiftTests/LocalReceiptParsing/ReceiptParserTests.swift b/PurchasesCoreSwiftTests/LocalReceiptParsing/ReceiptParserTests.swift index 9f84fb0aaa..a59a752d2b 100644 --- a/PurchasesCoreSwiftTests/LocalReceiptParsing/ReceiptParserTests.swift +++ b/PurchasesCoreSwiftTests/LocalReceiptParsing/ReceiptParserTests.swift @@ -28,7 +28,7 @@ class ReceiptParserTests: XCTestCase { ]) mockASN1ContainerBuilder.stubbedBuildResult = constructedContainer - let expectedReceipt = mockAppleReceipt() + let expectedReceipt = mockAppleReceiptWithoutPurchases() mockAppleReceiptBuilder.stubbedBuildResult = expectedReceipt let receivedReceipt = try! self.receiptParser.parse(from: Data()) @@ -64,7 +64,7 @@ class ReceiptParserTests: XCTestCase { ]) mockASN1ContainerBuilder.stubbedBuildResult = complexContainer - let expectedReceipt = mockAppleReceipt() + let expectedReceipt = mockAppleReceiptWithoutPurchases() mockAppleReceiptBuilder.stubbedBuildResult = expectedReceipt let receivedReceipt = try! self.receiptParser.parse(from: Data()) @@ -106,6 +106,23 @@ class ReceiptParserTests: XCTestCase { expect { try self.receiptParser.parse(from: Data()) } .to(throwError(ReceiptReadingError.dataObjectIdentifierMissing)) } + + func testReceiptHasTransactionsTrueIfReceiptHasTransactions() { + mockASN1ContainerBuilder.stubbedBuildResult = containerWithDataObjectIdentifier() + mockAppleReceiptBuilder.stubbedBuildResult = mockAppleReceiptWithPurchases() + expect(self.receiptParser.receiptHasTransactions(receiptData: Data())) == true + } + + func testReceiptHasTransactionsFalseIfNoIAPsInReceipt() { + mockASN1ContainerBuilder.stubbedBuildResult = containerWithDataObjectIdentifier() + mockAppleReceiptBuilder.stubbedBuildResult = mockAppleReceiptWithoutPurchases() + expect(self.receiptParser.receiptHasTransactions(receiptData: Data())) == false + } + + func testReceiptHasTransactionsFalseIfReceiptCantBeParsed() { + mockASN1ContainerBuilder.stubbedBuildError = ReceiptReadingError.receiptParsingError + expect(self.receiptParser.receiptHasTransactions(receiptData: Data())) == false + } } private extension ReceiptParserTests { @@ -119,7 +136,7 @@ private extension ReceiptParserTests { return constructedContainer } - func mockAppleReceipt() -> AppleReceipt { + func mockAppleReceiptWithoutPurchases() -> AppleReceipt { return AppleReceipt(bundleId: "com.revenuecat.testapp", applicationVersion: "3.2.3", originalApplicationVersion: "3.1.1", @@ -129,4 +146,42 @@ private extension ReceiptParserTests { expirationDate: nil, inAppPurchases: []) } + + func mockAppleReceiptWithPurchases() -> AppleReceipt { + return AppleReceipt(bundleId: "com.revenuecat.testapp", + applicationVersion: "3.2.3", + originalApplicationVersion: "3.1.1", + opaqueValue: Data(), + sha1Hash: Data(), + creationDate: Date(), + expirationDate: nil, + inAppPurchases: [ + InAppPurchase(quantity: 1, + productId: "com.revenuecat.test", + transactionId: "892398531", + originalTransactionId: "892398531", + productType: .autoRenewableSubscription, + purchaseDate: Date(), + originalPurchaseDate: Date(), + expiresDate: nil, + cancellationDate: Date(), + isInTrialPeriod: false, + isInIntroOfferPeriod: false, + webOrderLineItemId: 79238531, + promotionalOfferIdentifier: nil), + InAppPurchase(quantity: 1, + productId: "com.revenuecat.test", + transactionId: "892398532", + originalTransactionId: "892398531", + productType: .autoRenewableSubscription, + purchaseDate: Date(), + originalPurchaseDate: Date(), + expiresDate: nil, + cancellationDate: Date(), + isInTrialPeriod: false, + isInIntroOfferPeriod: false, + webOrderLineItemId: 79238532, + promotionalOfferIdentifier: nil) + ]) + } } From 6f251c28f09053d6015692422bc57b4e5011bce8 Mon Sep 17 00:00:00 2001 From: Andy Boedo Date: Thu, 27 Aug 2020 17:06:42 -0300 Subject: [PATCH 4/6] injected ReceiptParser through DI for testing purposes --- Purchases.xcodeproj/project.pbxproj | 4 ++ .../RCPurchases+Protected.h | 6 ++- Purchases/Public/RCPurchases.m | 11 ++-- .../Mocks/MockReceiptParser.swift | 1 + PurchasesTests/Mocks/MockReceiptParser.swift | 30 +++++++++++ .../Purchasing/PurchasesTests.swift | 53 +++++-------------- .../PurchasesSubscriberAttributesTests.swift | 7 ++- 7 files changed, 65 insertions(+), 47 deletions(-) create mode 100644 PurchasesTests/Mocks/MockReceiptParser.swift diff --git a/Purchases.xcodeproj/project.pbxproj b/Purchases.xcodeproj/project.pbxproj index fc67c658a2..5ff51805c3 100644 --- a/Purchases.xcodeproj/project.pbxproj +++ b/Purchases.xcodeproj/project.pbxproj @@ -201,6 +201,7 @@ 37E35F20B49BCE1B6D76B084 /* RCStoreKitRequestFetcher.h in Headers */ = {isa = PBXBuildFile; fileRef = 37E35804C14F5E6CEAF3909C /* RCStoreKitRequestFetcher.h */; settings = {ATTRIBUTES = (Private, ); }; }; 37E35F20FB949985BEEB4B58 /* MockRequestFetcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37E35609E46E869675A466C1 /* MockRequestFetcher.swift */; }; 37E35F549AEB655AB6DA83B3 /* MockSKDiscount.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37E35EABF6D7AFE367718784 /* MockSKDiscount.swift */; }; + 37E35F67255A87BD86B39D43 /* MockReceiptParser.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37E3558F697A939D2BBD7FEC /* MockReceiptParser.swift */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -374,6 +375,7 @@ 37E35548F15DE7CFFCE3AA8A /* NSLocale+RCExtensions.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "NSLocale+RCExtensions.m"; sourceTree = ""; }; 37E3555B4BE0A4F7222E7B00 /* MockOfferingsFactory.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MockOfferingsFactory.swift; sourceTree = ""; }; 37E355744D64075AA91342DE /* MockInAppPurchaseBuilder.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MockInAppPurchaseBuilder.swift; sourceTree = ""; }; + 37E3558F697A939D2BBD7FEC /* MockReceiptParser.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MockReceiptParser.swift; sourceTree = ""; }; 37E355AE6CB674484555D1AC /* NSDate+RCExtensions.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "NSDate+RCExtensions.h"; sourceTree = ""; }; 37E355CBB3F3A31A32687B14 /* Transaction.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Transaction.swift; sourceTree = ""; }; 37E35609E46E869675A466C1 /* MockRequestFetcher.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MockRequestFetcher.swift; sourceTree = ""; }; @@ -784,6 +786,7 @@ 2DD7BA4C24C63A830066B4C2 /* MockSystemInfo.swift */, 37E35659EB530A5109AFAB50 /* MockOperationDispatcher.swift */, 2D4D6AF224F7172900B656BE /* MockProductsRequest.swift */, + 37E3558F697A939D2BBD7FEC /* MockReceiptParser.swift */, ); path = Mocks; sourceTree = ""; @@ -1428,6 +1431,7 @@ 37E35F0387D0ADE014186924 /* ProductInfoExtractorTests.swift in Sources */, 37E351505CB4764821451E27 /* ProductInfoExtensions.swift in Sources */, 37E3599326581376E0142EEC /* SystemInfoTests.swift in Sources */, + 37E35F67255A87BD86B39D43 /* MockReceiptParser.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/Purchases/ProtectedExtensions/RCPurchases+Protected.h b/Purchases/ProtectedExtensions/RCPurchases+Protected.h index dd6e6496d1..0c15ee0f2b 100644 --- a/Purchases/ProtectedExtensions/RCPurchases+Protected.h +++ b/Purchases/ProtectedExtensions/RCPurchases+Protected.h @@ -19,7 +19,8 @@ RCSubscriberAttributesManager, RCSystemInfo, RCOperationDispatcher, - RCIntroEligibilityCalculator; + RCIntroEligibilityCalculator, + RCReceiptParser; NS_ASSUME_NONNULL_BEGIN @@ -40,7 +41,8 @@ NS_ASSUME_NONNULL_BEGIN identityManager:(RCIdentityManager *)identityManager subscriberAttributesManager:(RCSubscriberAttributesManager *)subscriberAttributesManager operationDispatcher:(RCOperationDispatcher *)operationDispatcher - introEligibilityCalculator:(RCIntroEligibilityCalculator *)introEligibilityCalculator; + introEligibilityCalculator:(RCIntroEligibilityCalculator *)introEligibilityCalculator + receiptParser:(RCReceiptParser *)receiptParser; + (void)setDefaultInstance:(nullable RCPurchases *)instance; diff --git a/Purchases/Public/RCPurchases.m b/Purchases/Public/RCPurchases.m index 62c4c5fd11..13af8966af 100644 --- a/Purchases/Public/RCPurchases.m +++ b/Purchases/Public/RCPurchases.m @@ -66,6 +66,7 @@ @interface RCPurchases () { @property (nonatomic) RCSystemInfo *systemInfo; @property (nonatomic) RCOperationDispatcher *operationDispatcher; @property (nonatomic) RCIntroEligibilityCalculator *introEligibilityCalculator; +@property (nonatomic) RCReceiptParser *receiptParser; @end @@ -225,6 +226,7 @@ - (instancetype)initWithAPIKey:(NSString *)APIKey deviceCache:deviceCache]; RCOperationDispatcher *operationDispatcher = [[RCOperationDispatcher alloc] init]; RCIntroEligibilityCalculator *introCalculator = [[RCIntroEligibilityCalculator alloc] init]; + RCReceiptParser *receiptParser = [[RCReceiptParser alloc] init]; return [self initWithAppUserID:appUserID requestFetcher:fetcher @@ -240,7 +242,8 @@ - (instancetype)initWithAPIKey:(NSString *)APIKey identityManager:identityManager subscriberAttributesManager:subscriberAttributesManager operationDispatcher:operationDispatcher - introEligibilityCalculator:introCalculator]; + introEligibilityCalculator:introCalculator + receiptParser:receiptParser]; } - (instancetype)initWithAppUserID:(nullable NSString *)appUserID @@ -257,7 +260,8 @@ - (instancetype)initWithAppUserID:(nullable NSString *)appUserID identityManager:(RCIdentityManager *)identityManager subscriberAttributesManager:(RCSubscriberAttributesManager *)subscriberAttributesManager operationDispatcher:(RCOperationDispatcher *)operationDispatcher - introEligibilityCalculator:(RCIntroEligibilityCalculator *)introEligibilityCalculator { + introEligibilityCalculator:(RCIntroEligibilityCalculator *)introEligibilityCalculator + receiptParser:(RCReceiptParser *)receiptParser { if (self = [super init]) { RCDebugLog(@"Debug logging enabled."); RCDebugLog(@"SDK Version - %@", self.class.frameworkVersion); @@ -283,6 +287,7 @@ - (instancetype)initWithAppUserID:(nullable NSString *)appUserID self.subscriberAttributesManager = subscriberAttributesManager; self.operationDispatcher = operationDispatcher; self.introEligibilityCalculator = introEligibilityCalculator; + self.receiptParser = receiptParser; RCReceivePurchaserInfoBlock callDelegate = ^void(RCPurchaserInfo *info, NSError *error) { if (info) { @@ -628,7 +633,7 @@ - (void)restoreTransactionsWithCompletionBlock:(nullable RCReceivePurchaserInfoB RCPurchaserInfo * _Nullable cachedPurchaserInfo = [self readPurchaserInfoFromCache]; BOOL hasOriginalPurchaseDate = cachedPurchaserInfo != nil && cachedPurchaserInfo.originalPurchaseDate != nil; - BOOL receiptHasTransactions = [[[RCReceiptParser alloc] init] receiptHasTransactionsWithReceiptData:data]; + BOOL receiptHasTransactions = [self.receiptParser receiptHasTransactionsWithReceiptData:data]; if (!receiptHasTransactions && hasOriginalPurchaseDate) { CALL_IF_SET_ON_MAIN_THREAD(completion, cachedPurchaserInfo, nil); return; diff --git a/PurchasesCoreSwiftTests/Mocks/MockReceiptParser.swift b/PurchasesCoreSwiftTests/Mocks/MockReceiptParser.swift index b7e5055220..109d84c2bf 100644 --- a/PurchasesCoreSwiftTests/Mocks/MockReceiptParser.swift +++ b/PurchasesCoreSwiftTests/Mocks/MockReceiptParser.swift @@ -27,6 +27,7 @@ class MockReceiptParser: ReceiptParser { containerBuilder: MockASN1ContainerBuilder(), receiptBuilder: MockAppleReceiptBuilder()) } + override func parse(from receiptData: Data) throws -> AppleReceipt { invokedParse = true invokedParseCount += 1 diff --git a/PurchasesTests/Mocks/MockReceiptParser.swift b/PurchasesTests/Mocks/MockReceiptParser.swift new file mode 100644 index 0000000000..901b922121 --- /dev/null +++ b/PurchasesTests/Mocks/MockReceiptParser.swift @@ -0,0 +1,30 @@ +// +// Created by Andrés Boedo on 8/27/20. +// Copyright (c) 2020 Purchases. All rights reserved. +// + +import Foundation +@testable import PurchasesCoreSwift + +class MockReceiptParser: ReceiptParser { + + init() { + super.init(objectIdentifierBuilder: ASN1ObjectIdentifierBuilder(), + containerBuilder: ASN1ContainerBuilder(), + receiptBuilder: AppleReceiptBuilder()) + } + + var invokedReceiptHasTransactions = false + var invokedReceiptHasTransactionsCount = 0 + var invokedReceiptHasTransactionsParameters: (receiptData: Data, Void)? + var invokedReceiptHasTransactionsParametersList = [(receiptData: Data, Void)]() + var stubbedReceiptHasTransactionsResult: Bool! = false + + override func receiptHasTransactions(receiptData: Data) -> Bool { + invokedReceiptHasTransactions = true + invokedReceiptHasTransactionsCount += 1 + invokedReceiptHasTransactionsParameters = (receiptData, ()) + invokedReceiptHasTransactionsParametersList.append((receiptData, ())) + return stubbedReceiptHasTransactionsResult + } +} diff --git a/PurchasesTests/Purchasing/PurchasesTests.swift b/PurchasesTests/Purchasing/PurchasesTests.swift index b8fbac75e9..12de2e1f17 100644 --- a/PurchasesTests/Purchasing/PurchasesTests.swift +++ b/PurchasesTests/Purchasing/PurchasesTests.swift @@ -16,6 +16,7 @@ class PurchasesTests: XCTestCase { systemInfo = MockSystemInfo(platformFlavor: nil, platformFlavorVersion: nil, finishTransactions: true) mockOperationDispatcher = MockOperationDispatcher() mockIntroEligibilityCalculator = MockIntroEligibilityCalculator() + mockReceiptParser = MockReceiptParser() } override func tearDown() { @@ -186,7 +187,8 @@ class PurchasesTests: XCTestCase { var systemInfo: MockSystemInfo! var mockOperationDispatcher: MockOperationDispatcher! var mockIntroEligibilityCalculator: MockIntroEligibilityCalculator! - + var mockReceiptParser: MockReceiptParser! + let purchasesDelegate = MockPurchasesDelegate() var purchases: Purchases! @@ -194,53 +196,23 @@ class PurchasesTests: XCTestCase { func setupPurchases(automaticCollection: Bool = false) { Purchases.automaticAppleSearchAdsAttributionCollection = automaticCollection self.identityManager.mockIsAnonymous = false - - purchases = Purchases(appUserID: identityManager.currentAppUserID, - requestFetcher: requestFetcher, - receiptFetcher: receiptFetcher, - attributionFetcher: attributionFetcher, - backend: backend, - storeKitWrapper: storeKitWrapper, - notificationCenter: notificationCenter, - userDefaults: userDefaults, - systemInfo: systemInfo, - offeringsFactory: offeringsFactory, - deviceCache: deviceCache, - identityManager: identityManager, - subscriberAttributesManager: subscriberAttributesManager, - operationDispatcher: mockOperationDispatcher, - introEligibilityCalculator: mockIntroEligibilityCalculator) - purchases!.delegate = purchasesDelegate - Purchases.setDefaultInstance(purchases!) + + initializePurchasesInstance(appUserId: identityManager.currentAppUserID) } func setupAnonPurchases() { Purchases.automaticAppleSearchAdsAttributionCollection = false self.identityManager.mockIsAnonymous = true - - purchases = Purchases(appUserID: nil, - requestFetcher: requestFetcher, - receiptFetcher: receiptFetcher, - attributionFetcher: attributionFetcher, - backend: backend, - storeKitWrapper: storeKitWrapper, - notificationCenter: notificationCenter, - userDefaults: userDefaults, - systemInfo: systemInfo, - offeringsFactory: offeringsFactory, - deviceCache: deviceCache, - identityManager: identityManager, - subscriberAttributesManager: subscriberAttributesManager, - operationDispatcher: mockOperationDispatcher, - introEligibilityCalculator: mockIntroEligibilityCalculator) - - purchases!.delegate = purchasesDelegate + initializePurchasesInstance(appUserId: nil) } func setupPurchasesObserverModeOn() { - let systemInfo = RCSystemInfo(platformFlavor: nil, platformFlavorVersion: nil, finishTransactions: false) + systemInfo = MockSystemInfo(platformFlavor: nil, platformFlavorVersion: nil, finishTransactions: false) + initializePurchasesInstance(appUserId: nil) + } - purchases = Purchases(appUserID: nil, + private func initializePurchasesInstance(appUserId: String?) { + purchases = Purchases(appUserID: appUserId, requestFetcher: requestFetcher, receiptFetcher: receiptFetcher, attributionFetcher: attributionFetcher, @@ -254,7 +226,8 @@ class PurchasesTests: XCTestCase { identityManager: identityManager, subscriberAttributesManager: subscriberAttributesManager, operationDispatcher: mockOperationDispatcher, - introEligibilityCalculator: mockIntroEligibilityCalculator) + introEligibilityCalculator: mockIntroEligibilityCalculator, + receiptParser: mockReceiptParser) purchases!.delegate = purchasesDelegate Purchases.setDefaultInstance(purchases!) diff --git a/PurchasesTests/SubscriberAttributes/PurchasesSubscriberAttributesTests.swift b/PurchasesTests/SubscriberAttributes/PurchasesSubscriberAttributesTests.swift index 03f46c9eb6..5eb00f0267 100644 --- a/PurchasesTests/SubscriberAttributes/PurchasesSubscriberAttributesTests.swift +++ b/PurchasesTests/SubscriberAttributes/PurchasesSubscriberAttributesTests.swift @@ -27,7 +27,8 @@ class PurchasesSubscriberAttributesTests: XCTestCase { let systemInfo: RCSystemInfo = RCSystemInfo(platformFlavor: nil, platformFlavorVersion: nil, finishTransactions: true) - + var mockReceiptParser: MockReceiptParser! + var mockOperationDispatcher: MockOperationDispatcher! var mockIntroEligibilityCalculator: MockIntroEligibilityCalculator! @@ -47,6 +48,7 @@ class PurchasesSubscriberAttributesTests: XCTestCase { ] self.mockOperationDispatcher = MockOperationDispatcher() self.mockIntroEligibilityCalculator = MockIntroEligibilityCalculator() + self.mockReceiptParser = MockReceiptParser() } override func tearDown() { @@ -73,7 +75,8 @@ class PurchasesSubscriberAttributesTests: XCTestCase { identityManager: mockIdentityManager, subscriberAttributesManager: mockSubscriberAttributesManager, operationDispatcher: mockOperationDispatcher, - introEligibilityCalculator: mockIntroEligibilityCalculator) + introEligibilityCalculator: mockIntroEligibilityCalculator, + receiptParser: mockReceiptParser) purchases!.delegate = purchasesDelegate Purchases.setDefaultInstance(purchases!) } From c1734557c8c4e270cf13ba70a14ac7b3208e5201 Mon Sep 17 00:00:00 2001 From: Andy Boedo Date: Fri, 28 Aug 2020 10:42:29 -0300 Subject: [PATCH 5/6] added tests to ensure that post gets called in the right circumstances --- Purchases/Public/RCPurchases.m | 4 +- .../Purchasing/PurchasesTests.swift | 62 +++++++++++++++++++ 2 files changed, 64 insertions(+), 2 deletions(-) diff --git a/Purchases/Public/RCPurchases.m b/Purchases/Public/RCPurchases.m index 13af8966af..6b2194f4d1 100644 --- a/Purchases/Public/RCPurchases.m +++ b/Purchases/Public/RCPurchases.m @@ -630,7 +630,7 @@ - (void)restoreTransactionsWithCompletionBlock:(nullable RCReceivePurchaserInfoB CALL_IF_SET_ON_MAIN_THREAD(completion, nil, [RCPurchasesErrorUtils missingReceiptFileError]); return; } - + RCPurchaserInfo * _Nullable cachedPurchaserInfo = [self readPurchaserInfoFromCache]; BOOL hasOriginalPurchaseDate = cachedPurchaserInfo != nil && cachedPurchaserInfo.originalPurchaseDate != nil; BOOL receiptHasTransactions = [self.receiptParser receiptHasTransactionsWithReceiptData:data]; @@ -638,7 +638,7 @@ - (void)restoreTransactionsWithCompletionBlock:(nullable RCReceivePurchaserInfoB CALL_IF_SET_ON_MAIN_THREAD(completion, cachedPurchaserInfo, nil); return; } - + RCSubscriberAttributeDict subscriberAttributes = self.unsyncedAttributesByKey; [self.backend postReceiptData:data appUserID:self.appUserID diff --git a/PurchasesTests/Purchasing/PurchasesTests.swift b/PurchasesTests/Purchasing/PurchasesTests.swift index 12de2e1f17..8d22d93754 100644 --- a/PurchasesTests/Purchasing/PurchasesTests.swift +++ b/PurchasesTests/Purchasing/PurchasesTests.swift @@ -902,6 +902,68 @@ class PurchasesTests: XCTestCase { expect(self.backend.postReceiptDataCalled).to(beTrue()) } + func testRestoringPurchasesDoesntPostIfReceiptEmptyAndPurchaserInfoLoaded() { + let info = Purchases.PurchaserInfo(data: [ + "subscriber": [ + "subscriptions": [:], + "other_purchases": [:], + "original_application_version": "1.0", + "original_purchase_date": "2018-10-26T23:17:53Z" + ]]); + + let jsonObject = info!.jsonObject() + + let object = try! JSONSerialization.data(withJSONObject: jsonObject, options: []); + self.deviceCache.cachedPurchaserInfo[identityManager.currentAppUserID] = object + + mockReceiptParser.stubbedReceiptHasTransactionsResult = false + + setupPurchases() + purchases!.restoreTransactions() + + expect(self.backend.postReceiptDataCalled) == false + } + + func testRestoringPurchasesPostsIfReceiptEmptyAndPurchaserInfoNotLoaded() { + mockReceiptParser.stubbedReceiptHasTransactionsResult = false + + setupPurchases() + purchases!.restoreTransactions() + + expect(self.backend.postReceiptDataCalled) == true + } + + func testRestoringPurchasesPostsIfReceiptHasTransactionsAndPurchaserInfoLoaded() { + let info = Purchases.PurchaserInfo(data: [ + "subscriber": [ + "subscriptions": [:], + "other_purchases": [:], + "original_application_version": "1.0", + "original_purchase_date": "2018-10-26T23:17:53Z" + ]]); + + let jsonObject = info!.jsonObject() + + let object = try! JSONSerialization.data(withJSONObject: jsonObject, options: []); + self.deviceCache.cachedPurchaserInfo[identityManager.currentAppUserID] = object + + mockReceiptParser.stubbedReceiptHasTransactionsResult = true + + setupPurchases() + purchases!.restoreTransactions() + + expect(self.backend.postReceiptDataCalled) == true + } + + func testRestoringPurchasesPostsIfReceiptHasTransactionsAndPurchaserInfoNotLoaded() { + mockReceiptParser.stubbedReceiptHasTransactionsResult = true + + setupPurchases() + purchases!.restoreTransactions() + + expect(self.backend.postReceiptDataCalled) == true + } + func testRestoringPurchasesAlwaysRefreshesAndPostsTheReceipt() { setupPurchases() self.receiptFetcher.shouldReturnReceipt = true From be920717ac41d448eaad20c9029103627e95cbb0 Mon Sep 17 00:00:00 2001 From: Andy Boedo Date: Fri, 28 Aug 2020 11:13:30 -0300 Subject: [PATCH 6/6] updated so that receiptHasTransactions returns true if the receipt can't be parsed --- PurchasesCoreSwift/LocalReceiptParsing/ReceiptParser.swift | 3 ++- .../LocalReceiptParsing/ReceiptParserTests.swift | 4 ++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/PurchasesCoreSwift/LocalReceiptParsing/ReceiptParser.swift b/PurchasesCoreSwift/LocalReceiptParsing/ReceiptParser.swift index 208412e9de..a9c124ed61 100644 --- a/PurchasesCoreSwift/LocalReceiptParsing/ReceiptParser.swift +++ b/PurchasesCoreSwift/LocalReceiptParsing/ReceiptParser.swift @@ -32,7 +32,8 @@ import Foundation if let receipt = try? parse(from: receiptData) { return receipt.inAppPurchases.count > 0 } - return false + // if the receipt can't be parsed, conservatively return true + return true } func parse(from receiptData: Data) throws -> AppleReceipt { diff --git a/PurchasesCoreSwiftTests/LocalReceiptParsing/ReceiptParserTests.swift b/PurchasesCoreSwiftTests/LocalReceiptParsing/ReceiptParserTests.swift index a59a752d2b..93f1181bd2 100644 --- a/PurchasesCoreSwiftTests/LocalReceiptParsing/ReceiptParserTests.swift +++ b/PurchasesCoreSwiftTests/LocalReceiptParsing/ReceiptParserTests.swift @@ -119,9 +119,9 @@ class ReceiptParserTests: XCTestCase { expect(self.receiptParser.receiptHasTransactions(receiptData: Data())) == false } - func testReceiptHasTransactionsFalseIfReceiptCantBeParsed() { + func testReceiptHasTransactionsTrueIfReceiptCantBeParsed() { mockASN1ContainerBuilder.stubbedBuildError = ReceiptReadingError.receiptParsingError - expect(self.receiptParser.receiptHasTransactions(receiptData: Data())) == false + expect(self.receiptParser.receiptHasTransactions(receiptData: Data())) == true } }