Skip to content

Implemented In-App-Purchase validation using App Store Receipt instead of Transaction Receipt. #87

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Aug 22, 2015
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion Parse/Internal/ParseManager.m
Original file line number Diff line number Diff line change
Expand Up @@ -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;
});
Expand Down
7 changes: 5 additions & 2 deletions Parse/Internal/Purchase/Controller/PFPurchaseController.h
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@

@property (nonatomic, strong, readonly) id<PFCommandRunning> commandRunner;
@property (nonatomic, strong, readonly) PFFileManager *fileManager;
@property (nonatomic, strong, readonly) NSBundle *bundle;

@property (nonatomic, strong) SKPaymentQueue *paymentQueue;
@property (nonatomic, strong, readonly) PFPaymentTransactionObserver *transactionObserver;
Expand All @@ -34,10 +35,12 @@

- (instancetype)init NS_UNAVAILABLE;
- (instancetype)initWithCommandRunner:(id<PFCommandRunning>)commandRunner
fileManager:(PFFileManager *)fileManager NS_DESIGNATED_INITIALIZER;
fileManager:(PFFileManager *)fileManager
bundle:(NSBundle *)bundle NS_DESIGNATED_INITIALIZER;

+ (instancetype)controllerWithCommandRunner:(id<PFCommandRunning>)commandRunner
fileManager:(PFFileManager *)fileManager;
fileManager:(PFFileManager *)fileManager
bundle:(NSBundle *)bundle;

///--------------------------------------
/// @name Products
Expand Down
44 changes: 29 additions & 15 deletions Parse/Internal/Purchase/Controller/PFPurchaseController.m
Original file line number Diff line number Diff line change
Expand Up @@ -46,19 +46,23 @@ - (instancetype)init {
PFNotDesignatedInitializer();
}

- (instancetype)initWithCommandRunner:(id<PFCommandRunning>)commandRunner fileManager:(PFFileManager *)fileManager {
- (instancetype)initWithCommandRunner:(id<PFCommandRunning>)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<PFCommandRunning>)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];
}

///--------------------------------------
Expand Down Expand Up @@ -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
Expand Down
76 changes: 68 additions & 8 deletions Tests/Unit/PurchaseControllerTests.m
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,8 @@ - (void)setUp {
- (void)tearDown {
PFTestSKProductsRequest.validProducts = nil;

[[NSFileManager defaultManager] removeItemAtPath:[self sampleReceiptFilePath] error:nil];

[super tearDown];
}

Expand All @@ -73,20 +75,27 @@ - (NSData *)sampleData {
return [NSData dataWithBytes:sampleData length:sizeof(sampleData)];
}

- (NSString *)sampleReceiptFilePath {
return [NSTemporaryDirectory() stringByAppendingPathComponent:@"receipt.data"];
}

///--------------------------------------
#pragma mark - Tests
///--------------------------------------

- (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]);
Expand All @@ -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];

Expand All @@ -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]);
Expand Down Expand Up @@ -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]);
Expand All @@ -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]]);

Expand Down Expand Up @@ -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]);

Expand All @@ -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
Expand All @@ -275,17 +330,22 @@ - (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]);

SKPayment *payment = [SKPayment paymentWithProduct:[self sampleProduct]];
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
Expand Down
4 changes: 3 additions & 1 deletion Tests/Unit/PurchaseUnitTests.m
Original file line number Diff line number Diff line change
Expand Up @@ -34,9 +34,11 @@ @implementation PurchaseUnitTests
- (PFPurchaseController *)mockedPurchaseController {
id<PFCommandRunning> 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;
Expand Down