From a34fe4e603b11448f2d2310d961ef4c70cb59f0e Mon Sep 17 00:00:00 2001 From: Nikita Lutsenko Date: Fri, 21 Aug 2015 14:48:14 -0700 Subject: [PATCH] Implemented In-App-Purchase validation using App Store Receipt instead of Transaction Receipt. --- Parse/Internal/ParseManager.m | 3 +- .../Controller/PFPurchaseController.h | 7 +- .../Controller/PFPurchaseController.m | 44 +++++++---- Tests/Unit/PurchaseControllerTests.m | 76 +++++++++++++++++-- Tests/Unit/PurchaseUnitTests.m | 4 +- 5 files changed, 107 insertions(+), 27 deletions(-) diff --git a/Parse/Internal/ParseManager.m b/Parse/Internal/ParseManager.m index bfac5848f..2fc2add14 100644 --- a/Parse/Internal/ParseManager.m +++ b/Parse/Internal/ParseManager.m @@ -342,7 +342,8 @@ - (PFPurchaseController *)purchaseController { dispatch_sync(_controllerAccessQueue, ^{ if (!_purchaseController) { _purchaseController = [PFPurchaseController controllerWithCommandRunner:self.commandRunner - fileManager:self.fileManager]; + fileManager:self.fileManager + bundle:[NSBundle mainBundle]]; } controller = _purchaseController; }); diff --git a/Parse/Internal/Purchase/Controller/PFPurchaseController.h b/Parse/Internal/Purchase/Controller/PFPurchaseController.h index e3b1aee3c..c12e79f9a 100644 --- a/Parse/Internal/Purchase/Controller/PFPurchaseController.h +++ b/Parse/Internal/Purchase/Controller/PFPurchaseController.h @@ -22,6 +22,7 @@ @property (nonatomic, strong, readonly) id commandRunner; @property (nonatomic, strong, readonly) PFFileManager *fileManager; +@property (nonatomic, strong, readonly) NSBundle *bundle; @property (nonatomic, strong) SKPaymentQueue *paymentQueue; @property (nonatomic, strong, readonly) PFPaymentTransactionObserver *transactionObserver; @@ -34,10 +35,12 @@ - (instancetype)init NS_UNAVAILABLE; - (instancetype)initWithCommandRunner:(id)commandRunner - fileManager:(PFFileManager *)fileManager NS_DESIGNATED_INITIALIZER; + fileManager:(PFFileManager *)fileManager + bundle:(NSBundle *)bundle NS_DESIGNATED_INITIALIZER; + (instancetype)controllerWithCommandRunner:(id)commandRunner - fileManager:(PFFileManager *)fileManager; + fileManager:(PFFileManager *)fileManager + bundle:(NSBundle *)bundle; ///-------------------------------------- /// @name Products diff --git a/Parse/Internal/Purchase/Controller/PFPurchaseController.m b/Parse/Internal/Purchase/Controller/PFPurchaseController.m index 7f5bf6dd6..58f211026 100644 --- a/Parse/Internal/Purchase/Controller/PFPurchaseController.m +++ b/Parse/Internal/Purchase/Controller/PFPurchaseController.m @@ -46,19 +46,23 @@ - (instancetype)init { PFNotDesignatedInitializer(); } -- (instancetype)initWithCommandRunner:(id)commandRunner fileManager:(PFFileManager *)fileManager { +- (instancetype)initWithCommandRunner:(id)commandRunner + fileManager:(PFFileManager *)fileManager + bundle:(NSBundle *)bundle { self = [super init]; if (!self) return nil; _commandRunner = commandRunner; _fileManager = fileManager; + _bundle = bundle; return self; } + (instancetype)controllerWithCommandRunner:(id)commandRunner - fileManager:(PFFileManager *)fileManager { - return [[self alloc] initWithCommandRunner:commandRunner fileManager:fileManager]; + fileManager:(PFFileManager *)fileManager + bundle:(NSBundle *)bundle { + return [[self alloc] initWithCommandRunner:commandRunner fileManager:fileManager bundle:bundle]; } ///-------------------------------------- @@ -137,20 +141,30 @@ - (BFTask *)buyProductAsyncWithIdentifier:(NSString *)productIdentifier { - (BFTask *)downloadAssetAsyncForTransaction:(SKPaymentTransaction *)transaction withProgressBlock:(PFProgressBlock)progressBlock sessionToken:(NSString *)sessionToken { - // Ignore the deprecation, as it works until iOS 9. - // TODO: (nlutsenko) Update for iOS 9 receipt verification. This will require server-side change, most likely. -#pragma clang diagnostic push -#pragma clang diagnostic ignored "-Wdeprecated-declarations" - NSData *transactionReceipt = transaction.transactionReceipt; -#pragma clang diagnostic pop - if (!transactionReceipt) { - NSError *error = [NSError errorWithDomain:PFParseErrorDomain - code:kPFErrorReceiptMissing - userInfo:nil]; - return [BFTask taskWithError:error]; + NSString *productIdentifier = transaction.payment.productIdentifier; + NSURL *appStoreReceiptURL = [self.bundle appStoreReceiptURL]; + if (!productIdentifier || !appStoreReceiptURL) { + return [BFTask taskWithError:[NSError errorWithDomain:PFParseErrorDomain + code:kPFErrorReceiptMissing + userInfo:nil]]; + } + + NSError *error = nil; + NSData *appStoreReceipt = [NSData dataWithContentsOfURL:appStoreReceiptURL + options:NSDataReadingMappedIfSafe + error:&error]; + if (!appStoreReceipt || error) { + NSDictionary *userInfo = nil; + if (error) { + userInfo = @{ NSUnderlyingErrorKey : error }; + } + return [BFTask taskWithError:[NSError errorWithDomain:PFParseErrorDomain + code:kPFErrorReceiptMissing + userInfo:userInfo]]; } - NSDictionary *params = [[PFEncoder objectEncoder] encodeObject:@{ @"receipt" : transactionReceipt }]; + NSDictionary *params = [[PFEncoder objectEncoder] encodeObject:@{ @"receipt" : appStoreReceipt, + @"productIdentifier" : productIdentifier }]; PFRESTCommand *command = [PFRESTCommand commandWithHTTPPath:@"validate_purchase" httpMethod:PFHTTPRequestMethodPOST parameters:params diff --git a/Tests/Unit/PurchaseControllerTests.m b/Tests/Unit/PurchaseControllerTests.m index 27467d1be..d907b7ce8 100644 --- a/Tests/Unit/PurchaseControllerTests.m +++ b/Tests/Unit/PurchaseControllerTests.m @@ -48,6 +48,8 @@ - (void)setUp { - (void)tearDown { PFTestSKProductsRequest.validProducts = nil; + [[NSFileManager defaultManager] removeItemAtPath:[self sampleReceiptFilePath] error:nil]; + [super tearDown]; } @@ -73,6 +75,10 @@ - (NSData *)sampleData { return [NSData dataWithBytes:sampleData length:sizeof(sampleData)]; } +- (NSString *)sampleReceiptFilePath { + return [NSTemporaryDirectory() stringByAppendingPathComponent:@"receipt.data"]; +} + ///-------------------------------------- #pragma mark - Tests ///-------------------------------------- @@ -80,13 +86,16 @@ - (NSData *)sampleData { - (void)testConstructor { id commandRunner = PFStrictProtocolMock(@protocol(PFCommandRunning)); id fileManager = PFClassMock([PFFileManager class]); + id bundle = PFStrictClassMock([NSBundle class]); PFPurchaseController *controller = [[PFPurchaseController alloc] initWithCommandRunner:commandRunner - fileManager:fileManager]; + fileManager:fileManager + bundle:bundle]; XCTAssertNotNil(controller); XCTAssertEqual(controller.commandRunner, commandRunner); XCTAssertEqual(controller.fileManager, fileManager); + XCTAssertEqual(controller.bundle, bundle); // This makes the test less sad. controller.paymentQueue = PFClassMock([SKPaymentQueue class]); @@ -98,9 +107,11 @@ - (void)testConstructor { - (void)testFindProductsAsync { id commandRunner = PFStrictProtocolMock(@protocol(PFCommandRunning)); id fileManager = PFStrictClassMock([PFFileManager class]); + id bundle = PFStrictClassMock([NSBundle class]); PFPurchaseController *purchaseController = [PFPurchaseController controllerWithCommandRunner:commandRunner - fileManager:fileManager]; + fileManager:fileManager + bundle:bundle]; purchaseController.productsRequestClass = [PFTestSKProductsRequest class]; @@ -123,9 +134,11 @@ - (void)testFindProductsAsync { - (void)testBuyProductsAsync { id commandRunner = PFStrictProtocolMock(@protocol(PFCommandRunning)); id fileManager = PFStrictClassMock([PFFileManager class]); + id bundle = PFStrictClassMock([NSBundle class]); PFPurchaseController *purchaseController = [[PFPurchaseController alloc] initWithCommandRunner:commandRunner - fileManager:fileManager]; + fileManager:fileManager + bundle:bundle]; purchaseController.productsRequestClass = [PFTestSKProductsRequest class]; purchaseController.paymentQueue = PFStrictClassMock([SKPaymentQueue class]); @@ -189,9 +202,11 @@ - (void)testBuyProductsAsync { - (void)testDownloadAssetAsync { id commandRunner = PFStrictProtocolMock(@protocol(PFCommandRunning)); id fileManager = PFStrictClassMock([PFFileManager class]); + id bundle = PFStrictClassMock([NSBundle class]); PFPurchaseController *purchaseController = [[PFPurchaseController alloc] initWithCommandRunner:commandRunner - fileManager:fileManager]; + fileManager:fileManager + bundle:bundle]; purchaseController.productsRequestClass = [PFTestSKProductsRequest class]; purchaseController.paymentQueue = PFStrictClassMock([SKPaymentQueue class]); @@ -200,7 +215,10 @@ - (void)testDownloadAssetAsync { PFTestSKPaymentTransaction *transaction = [PFTestSKPaymentTransaction transactionForPayment:payment withError:nil inState:SKPaymentTransactionStatePurchased]; - transaction.transactionReceipt = [self sampleData]; + + NSString *receiptFile = [self sampleReceiptFilePath]; + OCMStub([bundle appStoreReceiptURL]).andReturn([NSURL fileURLWithPath:receiptFile]); + [[self sampleData] writeToFile:receiptFile atomically:YES]; PFFile *mockedFile = PFPartialMock([PFFile fileWithName:@"testData" data:[self sampleData]]); @@ -245,9 +263,44 @@ - (void)testDownloadAssetAsync { - (void)testDownloadInvalidReceipt { id commandRunner = PFStrictProtocolMock(@protocol(PFCommandRunning)); id fileManager = PFStrictClassMock([PFFileManager class]); + id bundle = PFStrictClassMock([NSBundle class]); + + PFPurchaseController *purchaseController = [[PFPurchaseController alloc] initWithCommandRunner:commandRunner + fileManager:fileManager + bundle:bundle]; + purchaseController.productsRequestClass = [PFTestSKProductsRequest class]; + purchaseController.paymentQueue = PFStrictClassMock([SKPaymentQueue class]); + + SKPayment *payment = [SKPayment paymentWithProduct:[self sampleProduct]]; + PFTestSKPaymentTransaction *transaction = [PFTestSKPaymentTransaction transactionForPayment:payment + withError:nil + inState:SKPaymentTransactionStatePurchased]; + OCMStub([bundle appStoreReceiptURL]).andReturn(nil); + + XCTestExpectation *expectation = [self currentSelectorTestExpectation]; + [[purchaseController downloadAssetAsyncForTransaction:transaction + withProgressBlock:nil + sessionToken:@"token"] continueWithBlock:^id(BFTask *task) { + XCTAssertTrue(task.faulted); + XCTAssertNotNil(task.error); + XCTAssertEqual(task.error.code, kPFErrorReceiptMissing); + + [expectation fulfill]; + + return nil; + }]; + + [self waitForTestExpectations]; +} + +- (void)testDownloadMissingReceiptData { + id commandRunner = PFStrictProtocolMock(@protocol(PFCommandRunning)); + id fileManager = PFStrictClassMock([PFFileManager class]); + id bundle = PFStrictClassMock([NSBundle class]); PFPurchaseController *purchaseController = [[PFPurchaseController alloc] initWithCommandRunner:commandRunner - fileManager:fileManager]; + fileManager:fileManager + bundle:bundle]; purchaseController.productsRequestClass = [PFTestSKProductsRequest class]; purchaseController.paymentQueue = PFStrictClassMock([SKPaymentQueue class]); @@ -256,6 +309,8 @@ - (void)testDownloadInvalidReceipt { withError:nil inState:SKPaymentTransactionStatePurchased]; + OCMStub([bundle appStoreReceiptURL]).andReturn([NSURL fileURLWithPath:[self sampleReceiptFilePath]]); + XCTestExpectation *expectation = [self currentSelectorTestExpectation]; [[purchaseController downloadAssetAsyncForTransaction:transaction withProgressBlock:nil @@ -275,9 +330,11 @@ - (void)testDownloadInvalidReceipt { - (void)testDownloadInvalidFile { id commandRunner = PFStrictProtocolMock(@protocol(PFCommandRunning)); id fileManager = PFStrictClassMock([PFFileManager class]); + id bundle = PFStrictClassMock([NSBundle class]); PFPurchaseController *purchaseController = [[PFPurchaseController alloc] initWithCommandRunner:commandRunner - fileManager:fileManager]; + fileManager:fileManager + bundle:bundle]; purchaseController.productsRequestClass = [PFTestSKProductsRequest class]; purchaseController.paymentQueue = PFStrictClassMock([SKPaymentQueue class]); @@ -285,7 +342,10 @@ - (void)testDownloadInvalidFile { PFTestSKPaymentTransaction *transaction = [PFTestSKPaymentTransaction transactionForPayment:payment withError:nil inState:SKPaymentTransactionStatePurchased]; - transaction.transactionReceipt = [self sampleData]; + + NSString *temporaryFile = [self sampleReceiptFilePath]; + OCMStub([bundle appStoreReceiptURL]).andReturn([NSURL fileURLWithPath:temporaryFile]); + [[self sampleData] writeToFile:temporaryFile atomically:YES]; PFCommandResult *mockedResult = [PFCommandResult commandResultWithResult:@{ @"a" : @"Hello" } resultString:nil diff --git a/Tests/Unit/PurchaseUnitTests.m b/Tests/Unit/PurchaseUnitTests.m index bc2c80f8c..f23917585 100644 --- a/Tests/Unit/PurchaseUnitTests.m +++ b/Tests/Unit/PurchaseUnitTests.m @@ -34,9 +34,11 @@ @implementation PurchaseUnitTests - (PFPurchaseController *)mockedPurchaseController { id commandRunner = PFStrictProtocolMock(@protocol(PFCommandRunning)); PFFileManager *fileManager = PFStrictClassMock([PFFileManager class]); + id bundle = PFStrictClassMock([NSBundle class]); PFPurchaseController *purchaseController = PFPartialMock([[PFPurchaseController alloc] initWithCommandRunner:commandRunner - fileManager:fileManager]); + fileManager:fileManager + bundle:bundle]); SKPaymentQueue *paymentQueue = PFClassMock([SKPaymentQueue class]); purchaseController.paymentQueue = paymentQueue;