Skip to content

Commit 8d38b5f

Browse files
committed
Merge pull request #87 from ParsePlatform/nlutsenko.inapp
Implemented In-App-Purchase validation using App Store Receipt instead of Transaction Receipt.
2 parents f16f657 + a34fe4e commit 8d38b5f

File tree

5 files changed

+107
-27
lines changed

5 files changed

+107
-27
lines changed

Parse/Internal/ParseManager.m

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -342,7 +342,8 @@ - (PFPurchaseController *)purchaseController {
342342
dispatch_sync(_controllerAccessQueue, ^{
343343
if (!_purchaseController) {
344344
_purchaseController = [PFPurchaseController controllerWithCommandRunner:self.commandRunner
345-
fileManager:self.fileManager];
345+
fileManager:self.fileManager
346+
bundle:[NSBundle mainBundle]];
346347
}
347348
controller = _purchaseController;
348349
});

Parse/Internal/Purchase/Controller/PFPurchaseController.h

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222

2323
@property (nonatomic, strong, readonly) id<PFCommandRunning> commandRunner;
2424
@property (nonatomic, strong, readonly) PFFileManager *fileManager;
25+
@property (nonatomic, strong, readonly) NSBundle *bundle;
2526

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

3536
- (instancetype)init NS_UNAVAILABLE;
3637
- (instancetype)initWithCommandRunner:(id<PFCommandRunning>)commandRunner
37-
fileManager:(PFFileManager *)fileManager NS_DESIGNATED_INITIALIZER;
38+
fileManager:(PFFileManager *)fileManager
39+
bundle:(NSBundle *)bundle NS_DESIGNATED_INITIALIZER;
3840

3941
+ (instancetype)controllerWithCommandRunner:(id<PFCommandRunning>)commandRunner
40-
fileManager:(PFFileManager *)fileManager;
42+
fileManager:(PFFileManager *)fileManager
43+
bundle:(NSBundle *)bundle;
4144

4245
///--------------------------------------
4346
/// @name Products

Parse/Internal/Purchase/Controller/PFPurchaseController.m

Lines changed: 29 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -46,19 +46,23 @@ - (instancetype)init {
4646
PFNotDesignatedInitializer();
4747
}
4848

49-
- (instancetype)initWithCommandRunner:(id<PFCommandRunning>)commandRunner fileManager:(PFFileManager *)fileManager {
49+
- (instancetype)initWithCommandRunner:(id<PFCommandRunning>)commandRunner
50+
fileManager:(PFFileManager *)fileManager
51+
bundle:(NSBundle *)bundle {
5052
self = [super init];
5153
if (!self) return nil;
5254

5355
_commandRunner = commandRunner;
5456
_fileManager = fileManager;
57+
_bundle = bundle;
5558

5659
return self;
5760
}
5861

5962
+ (instancetype)controllerWithCommandRunner:(id<PFCommandRunning>)commandRunner
60-
fileManager:(PFFileManager *)fileManager {
61-
return [[self alloc] initWithCommandRunner:commandRunner fileManager:fileManager];
63+
fileManager:(PFFileManager *)fileManager
64+
bundle:(NSBundle *)bundle {
65+
return [[self alloc] initWithCommandRunner:commandRunner fileManager:fileManager bundle:bundle];
6266
}
6367

6468
///--------------------------------------
@@ -137,20 +141,30 @@ - (BFTask *)buyProductAsyncWithIdentifier:(NSString *)productIdentifier {
137141
- (BFTask *)downloadAssetAsyncForTransaction:(SKPaymentTransaction *)transaction
138142
withProgressBlock:(PFProgressBlock)progressBlock
139143
sessionToken:(NSString *)sessionToken {
140-
// Ignore the deprecation, as it works until iOS 9.
141-
// TODO: (nlutsenko) Update for iOS 9 receipt verification. This will require server-side change, most likely.
142-
#pragma clang diagnostic push
143-
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
144-
NSData *transactionReceipt = transaction.transactionReceipt;
145-
#pragma clang diagnostic pop
146-
if (!transactionReceipt) {
147-
NSError *error = [NSError errorWithDomain:PFParseErrorDomain
148-
code:kPFErrorReceiptMissing
149-
userInfo:nil];
150-
return [BFTask taskWithError:error];
144+
NSString *productIdentifier = transaction.payment.productIdentifier;
145+
NSURL *appStoreReceiptURL = [self.bundle appStoreReceiptURL];
146+
if (!productIdentifier || !appStoreReceiptURL) {
147+
return [BFTask taskWithError:[NSError errorWithDomain:PFParseErrorDomain
148+
code:kPFErrorReceiptMissing
149+
userInfo:nil]];
150+
}
151+
152+
NSError *error = nil;
153+
NSData *appStoreReceipt = [NSData dataWithContentsOfURL:appStoreReceiptURL
154+
options:NSDataReadingMappedIfSafe
155+
error:&error];
156+
if (!appStoreReceipt || error) {
157+
NSDictionary *userInfo = nil;
158+
if (error) {
159+
userInfo = @{ NSUnderlyingErrorKey : error };
160+
}
161+
return [BFTask taskWithError:[NSError errorWithDomain:PFParseErrorDomain
162+
code:kPFErrorReceiptMissing
163+
userInfo:userInfo]];
151164
}
152165

153-
NSDictionary *params = [[PFEncoder objectEncoder] encodeObject:@{ @"receipt" : transactionReceipt }];
166+
NSDictionary *params = [[PFEncoder objectEncoder] encodeObject:@{ @"receipt" : appStoreReceipt,
167+
@"productIdentifier" : productIdentifier }];
154168
PFRESTCommand *command = [PFRESTCommand commandWithHTTPPath:@"validate_purchase"
155169
httpMethod:PFHTTPRequestMethodPOST
156170
parameters:params

Tests/Unit/PurchaseControllerTests.m

Lines changed: 68 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,8 @@ - (void)setUp {
4848
- (void)tearDown {
4949
PFTestSKProductsRequest.validProducts = nil;
5050

51+
[[NSFileManager defaultManager] removeItemAtPath:[self sampleReceiptFilePath] error:nil];
52+
5153
[super tearDown];
5254
}
5355

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

78+
- (NSString *)sampleReceiptFilePath {
79+
return [NSTemporaryDirectory() stringByAppendingPathComponent:@"receipt.data"];
80+
}
81+
7682
///--------------------------------------
7783
#pragma mark - Tests
7884
///--------------------------------------
7985

8086
- (void)testConstructor {
8187
id commandRunner = PFStrictProtocolMock(@protocol(PFCommandRunning));
8288
id fileManager = PFClassMock([PFFileManager class]);
89+
id bundle = PFStrictClassMock([NSBundle class]);
8390

8491
PFPurchaseController *controller = [[PFPurchaseController alloc] initWithCommandRunner:commandRunner
85-
fileManager:fileManager];
92+
fileManager:fileManager
93+
bundle:bundle];
8694

8795
XCTAssertNotNil(controller);
8896
XCTAssertEqual(controller.commandRunner, commandRunner);
8997
XCTAssertEqual(controller.fileManager, fileManager);
98+
XCTAssertEqual(controller.bundle, bundle);
9099

91100
// This makes the test less sad.
92101
controller.paymentQueue = PFClassMock([SKPaymentQueue class]);
@@ -98,9 +107,11 @@ - (void)testConstructor {
98107
- (void)testFindProductsAsync {
99108
id commandRunner = PFStrictProtocolMock(@protocol(PFCommandRunning));
100109
id fileManager = PFStrictClassMock([PFFileManager class]);
110+
id bundle = PFStrictClassMock([NSBundle class]);
101111

102112
PFPurchaseController *purchaseController = [PFPurchaseController controllerWithCommandRunner:commandRunner
103-
fileManager:fileManager];
113+
fileManager:fileManager
114+
bundle:bundle];
104115

105116
purchaseController.productsRequestClass = [PFTestSKProductsRequest class];
106117

@@ -123,9 +134,11 @@ - (void)testFindProductsAsync {
123134
- (void)testBuyProductsAsync {
124135
id commandRunner = PFStrictProtocolMock(@protocol(PFCommandRunning));
125136
id fileManager = PFStrictClassMock([PFFileManager class]);
137+
id bundle = PFStrictClassMock([NSBundle class]);
126138

127139
PFPurchaseController *purchaseController = [[PFPurchaseController alloc] initWithCommandRunner:commandRunner
128-
fileManager:fileManager];
140+
fileManager:fileManager
141+
bundle:bundle];
129142

130143
purchaseController.productsRequestClass = [PFTestSKProductsRequest class];
131144
purchaseController.paymentQueue = PFStrictClassMock([SKPaymentQueue class]);
@@ -189,9 +202,11 @@ - (void)testBuyProductsAsync {
189202
- (void)testDownloadAssetAsync {
190203
id commandRunner = PFStrictProtocolMock(@protocol(PFCommandRunning));
191204
id fileManager = PFStrictClassMock([PFFileManager class]);
205+
id bundle = PFStrictClassMock([NSBundle class]);
192206

193207
PFPurchaseController *purchaseController = [[PFPurchaseController alloc] initWithCommandRunner:commandRunner
194-
fileManager:fileManager];
208+
fileManager:fileManager
209+
bundle:bundle];
195210

196211
purchaseController.productsRequestClass = [PFTestSKProductsRequest class];
197212
purchaseController.paymentQueue = PFStrictClassMock([SKPaymentQueue class]);
@@ -200,7 +215,10 @@ - (void)testDownloadAssetAsync {
200215
PFTestSKPaymentTransaction *transaction = [PFTestSKPaymentTransaction transactionForPayment:payment
201216
withError:nil
202217
inState:SKPaymentTransactionStatePurchased];
203-
transaction.transactionReceipt = [self sampleData];
218+
219+
NSString *receiptFile = [self sampleReceiptFilePath];
220+
OCMStub([bundle appStoreReceiptURL]).andReturn([NSURL fileURLWithPath:receiptFile]);
221+
[[self sampleData] writeToFile:receiptFile atomically:YES];
204222

205223
PFFile *mockedFile = PFPartialMock([PFFile fileWithName:@"testData" data:[self sampleData]]);
206224

@@ -245,9 +263,44 @@ - (void)testDownloadAssetAsync {
245263
- (void)testDownloadInvalidReceipt {
246264
id commandRunner = PFStrictProtocolMock(@protocol(PFCommandRunning));
247265
id fileManager = PFStrictClassMock([PFFileManager class]);
266+
id bundle = PFStrictClassMock([NSBundle class]);
267+
268+
PFPurchaseController *purchaseController = [[PFPurchaseController alloc] initWithCommandRunner:commandRunner
269+
fileManager:fileManager
270+
bundle:bundle];
271+
purchaseController.productsRequestClass = [PFTestSKProductsRequest class];
272+
purchaseController.paymentQueue = PFStrictClassMock([SKPaymentQueue class]);
273+
274+
SKPayment *payment = [SKPayment paymentWithProduct:[self sampleProduct]];
275+
PFTestSKPaymentTransaction *transaction = [PFTestSKPaymentTransaction transactionForPayment:payment
276+
withError:nil
277+
inState:SKPaymentTransactionStatePurchased];
278+
OCMStub([bundle appStoreReceiptURL]).andReturn(nil);
279+
280+
XCTestExpectation *expectation = [self currentSelectorTestExpectation];
281+
[[purchaseController downloadAssetAsyncForTransaction:transaction
282+
withProgressBlock:nil
283+
sessionToken:@"token"] continueWithBlock:^id(BFTask *task) {
284+
XCTAssertTrue(task.faulted);
285+
XCTAssertNotNil(task.error);
286+
XCTAssertEqual(task.error.code, kPFErrorReceiptMissing);
287+
288+
[expectation fulfill];
289+
290+
return nil;
291+
}];
292+
293+
[self waitForTestExpectations];
294+
}
295+
296+
- (void)testDownloadMissingReceiptData {
297+
id commandRunner = PFStrictProtocolMock(@protocol(PFCommandRunning));
298+
id fileManager = PFStrictClassMock([PFFileManager class]);
299+
id bundle = PFStrictClassMock([NSBundle class]);
248300

249301
PFPurchaseController *purchaseController = [[PFPurchaseController alloc] initWithCommandRunner:commandRunner
250-
fileManager:fileManager];
302+
fileManager:fileManager
303+
bundle:bundle];
251304
purchaseController.productsRequestClass = [PFTestSKProductsRequest class];
252305
purchaseController.paymentQueue = PFStrictClassMock([SKPaymentQueue class]);
253306

@@ -256,6 +309,8 @@ - (void)testDownloadInvalidReceipt {
256309
withError:nil
257310
inState:SKPaymentTransactionStatePurchased];
258311

312+
OCMStub([bundle appStoreReceiptURL]).andReturn([NSURL fileURLWithPath:[self sampleReceiptFilePath]]);
313+
259314
XCTestExpectation *expectation = [self currentSelectorTestExpectation];
260315
[[purchaseController downloadAssetAsyncForTransaction:transaction
261316
withProgressBlock:nil
@@ -275,17 +330,22 @@ - (void)testDownloadInvalidReceipt {
275330
- (void)testDownloadInvalidFile {
276331
id commandRunner = PFStrictProtocolMock(@protocol(PFCommandRunning));
277332
id fileManager = PFStrictClassMock([PFFileManager class]);
333+
id bundle = PFStrictClassMock([NSBundle class]);
278334

279335
PFPurchaseController *purchaseController = [[PFPurchaseController alloc] initWithCommandRunner:commandRunner
280-
fileManager:fileManager];
336+
fileManager:fileManager
337+
bundle:bundle];
281338
purchaseController.productsRequestClass = [PFTestSKProductsRequest class];
282339
purchaseController.paymentQueue = PFStrictClassMock([SKPaymentQueue class]);
283340

284341
SKPayment *payment = [SKPayment paymentWithProduct:[self sampleProduct]];
285342
PFTestSKPaymentTransaction *transaction = [PFTestSKPaymentTransaction transactionForPayment:payment
286343
withError:nil
287344
inState:SKPaymentTransactionStatePurchased];
288-
transaction.transactionReceipt = [self sampleData];
345+
346+
NSString *temporaryFile = [self sampleReceiptFilePath];
347+
OCMStub([bundle appStoreReceiptURL]).andReturn([NSURL fileURLWithPath:temporaryFile]);
348+
[[self sampleData] writeToFile:temporaryFile atomically:YES];
289349

290350
PFCommandResult *mockedResult = [PFCommandResult commandResultWithResult:@{ @"a" : @"Hello" }
291351
resultString:nil

Tests/Unit/PurchaseUnitTests.m

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,9 +34,11 @@ @implementation PurchaseUnitTests
3434
- (PFPurchaseController *)mockedPurchaseController {
3535
id<PFCommandRunning> commandRunner = PFStrictProtocolMock(@protocol(PFCommandRunning));
3636
PFFileManager *fileManager = PFStrictClassMock([PFFileManager class]);
37+
id bundle = PFStrictClassMock([NSBundle class]);
3738

3839
PFPurchaseController *purchaseController = PFPartialMock([[PFPurchaseController alloc] initWithCommandRunner:commandRunner
39-
fileManager:fileManager]);
40+
fileManager:fileManager
41+
bundle:bundle]);
4042

4143
SKPaymentQueue *paymentQueue = PFClassMock([SKPaymentQueue class]);
4244
purchaseController.paymentQueue = paymentQueue;

0 commit comments

Comments
 (0)