From 4536d7d13f9eb6a4ec4bf718c243985bd447ca06 Mon Sep 17 00:00:00 2001 From: Rene Floor Date: Thu, 24 Jun 2021 14:40:40 +0200 Subject: [PATCH 1/7] Fix app exceptions caused by missing App Store receipt Rebase of flutter/plugins/pull/2862 Co-authored-by: LHLL --- .../in_app_purchase_ios/CHANGELOG.md | 4 ++++ .../example/ios/RunnerTests/Stubs.m | 2 +- .../ios/Classes/FIAPReceiptManager.m | 16 ++++++++++------ .../in_app_purchase_ios_platform_addition.dart | 16 ++++++++++------ .../sk_methodchannel_apis_test.dart | 12 ++++++++++++ 5 files changed, 37 insertions(+), 13 deletions(-) diff --git a/packages/in_app_purchase/in_app_purchase_ios/CHANGELOG.md b/packages/in_app_purchase/in_app_purchase_ios/CHANGELOG.md index 4b2d8ce1dc24..2447a867c4d5 100644 --- a/packages/in_app_purchase/in_app_purchase_ios/CHANGELOG.md +++ b/packages/in_app_purchase/in_app_purchase_ios/CHANGELOG.md @@ -1,3 +1,7 @@ +## 0.1.0+3 + +* iOS: Fix treating missing App Store receipt as an exception. + ## 0.1.0+2 * Changed the iOS payment queue handler in such a way that it only adds a listener to the SKPaymentQueue when there diff --git a/packages/in_app_purchase/in_app_purchase_ios/example/ios/RunnerTests/Stubs.m b/packages/in_app_purchase/in_app_purchase_ios/example/ios/RunnerTests/Stubs.m index 8af326a48722..54f55381211d 100644 --- a/packages/in_app_purchase/in_app_purchase_ios/example/ios/RunnerTests/Stubs.m +++ b/packages/in_app_purchase/in_app_purchase_ios/example/ios/RunnerTests/Stubs.m @@ -259,7 +259,7 @@ - (instancetype)initWithMap:(NSDictionary *)map { @implementation FIAPReceiptManagerStub : FIAPReceiptManager -- (NSData *)getReceiptData:(NSURL *)url { +- (NSData *)getReceiptData:(NSURL *)url error:(NSError **)error { NSString *originalString = [NSString stringWithFormat:@"test"]; return [[NSData alloc] initWithBase64EncodedString:originalString options:kNilOptions]; } diff --git a/packages/in_app_purchase/in_app_purchase_ios/ios/Classes/FIAPReceiptManager.m b/packages/in_app_purchase/in_app_purchase_ios/ios/Classes/FIAPReceiptManager.m index 526364020ad3..ceee71af3c5c 100644 --- a/packages/in_app_purchase/in_app_purchase_ios/ios/Classes/FIAPReceiptManager.m +++ b/packages/in_app_purchase/in_app_purchase_ios/ios/Classes/FIAPReceiptManager.m @@ -9,18 +9,22 @@ @implementation FIAPReceiptManager - (NSString *)retrieveReceiptWithError:(FlutterError **)error { NSURL *receiptURL = [[NSBundle mainBundle] appStoreReceiptURL]; - NSData *receipt = [self getReceiptData:receiptURL]; + NSError *err; + NSData *receipt = [self getReceiptData:receiptURL error:&err]; + if (err) { + *error = [FlutterError errorWithCode:[[NSString alloc] initWithFormat:@"%li", (long)err.code] + message:err.domain + details:err.userInfo]; + return nil; + } if (!receipt) { - *error = [FlutterError errorWithCode:@"storekit_no_receipt" - message:@"Cannot find receipt for the current main bundle." - details:nil]; return nil; } return [receipt base64EncodedStringWithOptions:kNilOptions]; } -- (NSData *)getReceiptData:(NSURL *)url { - return [NSData dataWithContentsOfURL:url]; +- (NSData *)getReceiptData:(NSURL *)url error:(NSError **)error { + return [NSData dataWithContentsOfURL:url options:NSDataReadingMappedIfSafe error:error]; } @end diff --git a/packages/in_app_purchase/in_app_purchase_ios/lib/src/in_app_purchase_ios_platform_addition.dart b/packages/in_app_purchase/in_app_purchase_ios/lib/src/in_app_purchase_ios_platform_addition.dart index 0c7b2de860b6..629a2a60439b 100644 --- a/packages/in_app_purchase/in_app_purchase_ios/lib/src/in_app_purchase_ios_platform_addition.dart +++ b/packages/in_app_purchase/in_app_purchase_ios/lib/src/in_app_purchase_ios_platform_addition.dart @@ -21,13 +21,17 @@ class InAppPurchaseIosPlatformAddition extends InAppPurchasePlatformAddition { /// If no results, a `null` value is returned. Future refreshPurchaseVerificationData() async { await SKRequestMaker().startRefreshReceiptRequest(); - final String? receipt = await SKReceiptManager.retrieveReceiptData(); - if (receipt == null) { + try { + String receipt = await SKReceiptManager.retrieveReceiptData(); + return PurchaseVerificationData( + localVerificationData: receipt, + serverVerificationData: receipt, + source: kIAPSource); + } catch (e) { + print( + 'Something is wrong while fetching the receipt, this normally happens when the app is ' + 'running on a simulator: $e'); return null; } - return PurchaseVerificationData( - localVerificationData: receipt, - serverVerificationData: receipt, - source: kIAPSource); } } diff --git a/packages/in_app_purchase/in_app_purchase_ios/test/store_kit_wrappers/sk_methodchannel_apis_test.dart b/packages/in_app_purchase/in_app_purchase_ios/test/store_kit_wrappers/sk_methodchannel_apis_test.dart index edb50aeb62a0..a5a1927d365d 100644 --- a/packages/in_app_purchase/in_app_purchase_ios/test/store_kit_wrappers/sk_methodchannel_apis_test.dart +++ b/packages/in_app_purchase/in_app_purchase_ios/test/store_kit_wrappers/sk_methodchannel_apis_test.dart @@ -74,6 +74,12 @@ void main() { expect(fakeIOSPlatform.refreshReceiptParam, {"isExpired": true}); }); + + test('should get null receipt if any exceptions are raised', () async { + fakeIOSPlatform.getReceiptFailTest = true; + expect(() async => SKReceiptManager.retrieveReceiptData(), + throwsA(TypeMatcher())); + }); }); group('sk_receipt_manager', () { @@ -166,6 +172,9 @@ class FakeIOSPlatform { bool getProductRequestFailTest = false; bool testReturnNull = false; + // get receipt request + bool getReceiptFailTest = false; + // refresh receipt request int refreshReceipt = 0; late Map refreshReceiptParam; @@ -201,6 +210,9 @@ class FakeIOSPlatform { return Future.sync(() {}); // receipt manager case '-[InAppPurchasePlugin retrieveReceiptData:result:]': + if (getReceiptFailTest) { + throw ("some arbitrary error"); + } return Future.value('receipt data'); // payment queue case '-[SKPaymentQueue canMakePayments:]': From 9266b997bc55269253af5dc913516f7d709029e0 Mon Sep 17 00:00:00 2001 From: Rene Floor Date: Thu, 24 Jun 2021 17:13:38 +0200 Subject: [PATCH 2/7] Added test for error --- .../RunnerTests/InAppPurchasePluginTests.m | 26 ++++++++++++++++--- .../example/ios/RunnerTests/Stubs.h | 1 + .../example/ios/RunnerTests/Stubs.m | 3 +++ 3 files changed, 27 insertions(+), 3 deletions(-) diff --git a/packages/in_app_purchase/in_app_purchase_ios/example/ios/RunnerTests/InAppPurchasePluginTests.m b/packages/in_app_purchase/in_app_purchase_ios/example/ios/RunnerTests/InAppPurchasePluginTests.m index 241ea0d5cb0d..0e775d732283 100644 --- a/packages/in_app_purchase/in_app_purchase_ios/example/ios/RunnerTests/InAppPurchasePluginTests.m +++ b/packages/in_app_purchase/in_app_purchase_ios/example/ios/RunnerTests/InAppPurchasePluginTests.m @@ -11,6 +11,7 @@ @interface InAppPurchasePluginTest : XCTestCase +@property(strong, nonatomic) FIAPReceiptManagerStub* receiptManagerStub; @property(strong, nonatomic) InAppPurchasePlugin* plugin; @end @@ -18,8 +19,8 @@ @interface InAppPurchasePluginTest : XCTestCase @implementation InAppPurchasePluginTest - (void)setUp { - self.plugin = - [[InAppPurchasePluginStub alloc] initWithReceiptManager:[FIAPReceiptManagerStub new]]; + self.receiptManagerStub = [FIAPReceiptManagerStub new]; + self.plugin = [[InAppPurchasePluginStub alloc] initWithReceiptManager:self.receiptManagerStub]; } - (void)tearDown { @@ -219,7 +220,7 @@ - (void)testRestoreTransactions { XCTAssertTrue(callbackInvoked); } -- (void)testRetrieveReceiptData { +- (void)testRetrieveReceiptDataSuccess { XCTestExpectation* expectation = [self expectationWithDescription:@"receipt data retrieved"]; FlutterMethodCall* call = [FlutterMethodCall methodCallWithMethodName:@"-[InAppPurchasePlugin retrieveReceiptData:result:]" @@ -233,6 +234,25 @@ - (void)testRetrieveReceiptData { [self waitForExpectations:@[ expectation ] timeout:5]; NSLog(@"%@", result); XCTAssertNotNil(result); + XCTAssert([result isKindOfClass:[NSString class]]); +} + +- (void)testRetrieveReceiptDataError { + XCTestExpectation* expectation = [self expectationWithDescription:@"receipt data retrieved"]; + FlutterMethodCall* call = [FlutterMethodCall + methodCallWithMethodName:@"-[InAppPurchasePlugin retrieveReceiptData:result:]" + arguments:nil]; + __block NSDictionary* result; + self.receiptManagerStub.returnError = YES; + [self.plugin handleMethodCall:call + result:^(id r) { + result = r; + [expectation fulfill]; + }]; + [self waitForExpectations:@[ expectation ] timeout:5]; + NSLog(@"%@", result); + XCTAssertNotNil(result); + XCTAssert([result isKindOfClass:[FlutterError class]]); } - (void)testRefreshReceiptRequest { diff --git a/packages/in_app_purchase/in_app_purchase_ios/example/ios/RunnerTests/Stubs.h b/packages/in_app_purchase/in_app_purchase_ios/example/ios/RunnerTests/Stubs.h index 687118febb29..df5d26627a37 100644 --- a/packages/in_app_purchase/in_app_purchase_ios/example/ios/RunnerTests/Stubs.h +++ b/packages/in_app_purchase/in_app_purchase_ios/example/ios/RunnerTests/Stubs.h @@ -54,6 +54,7 @@ API_AVAILABLE(ios(11.2), macos(10.13.2)) @end @interface FIAPReceiptManagerStub : FIAPReceiptManager +@property(assign, nonatomic) BOOL returnError; @end @interface SKReceiptRefreshRequestStub : SKReceiptRefreshRequest diff --git a/packages/in_app_purchase/in_app_purchase_ios/example/ios/RunnerTests/Stubs.m b/packages/in_app_purchase/in_app_purchase_ios/example/ios/RunnerTests/Stubs.m index 54f55381211d..99ba99eb2ade 100644 --- a/packages/in_app_purchase/in_app_purchase_ios/example/ios/RunnerTests/Stubs.m +++ b/packages/in_app_purchase/in_app_purchase_ios/example/ios/RunnerTests/Stubs.m @@ -260,6 +260,9 @@ - (instancetype)initWithMap:(NSDictionary *)map { @implementation FIAPReceiptManagerStub : FIAPReceiptManager - (NSData *)getReceiptData:(NSURL *)url error:(NSError **)error { + if (self.returnError) { + *error = [[NSError alloc] init]; + } NSString *originalString = [NSString stringWithFormat:@"test"]; return [[NSData alloc] initWithBase64EncodedString:originalString options:kNilOptions]; } From a256308d2977446813171dbc23ac4bc557fe212b Mon Sep 17 00:00:00 2001 From: Rene Floor Date: Thu, 24 Jun 2021 17:15:52 +0200 Subject: [PATCH 3/7] update pubspec --- packages/in_app_purchase/in_app_purchase_ios/pubspec.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/in_app_purchase/in_app_purchase_ios/pubspec.yaml b/packages/in_app_purchase/in_app_purchase_ios/pubspec.yaml index 5b9e3892d40d..06991e218cc5 100644 --- a/packages/in_app_purchase/in_app_purchase_ios/pubspec.yaml +++ b/packages/in_app_purchase/in_app_purchase_ios/pubspec.yaml @@ -2,7 +2,7 @@ name: in_app_purchase_ios description: An implementation for the iOS platform of the Flutter `in_app_purchase` plugin. This uses the iOS StoreKit Framework. repository: https://github.com/flutter/plugins/tree/master/packages/in_app_purchase/in_app_purchase_ios issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+in_app_purchase%22 -version: 0.1.0+2 +version: 0.1.0+3 environment: sdk: ">=2.12.0 <3.0.0" From 5e65655c88752df9a62caad1987f3f233e0fab5c Mon Sep 17 00:00:00 2001 From: Rene Floor Date: Thu, 24 Jun 2021 17:29:43 +0200 Subject: [PATCH 4/7] Fix existing test --- .../test/store_kit_wrappers/sk_methodchannel_apis_test.dart | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/in_app_purchase/in_app_purchase_ios/test/store_kit_wrappers/sk_methodchannel_apis_test.dart b/packages/in_app_purchase/in_app_purchase_ios/test/store_kit_wrappers/sk_methodchannel_apis_test.dart index a5a1927d365d..76e1b0e993a4 100644 --- a/packages/in_app_purchase/in_app_purchase_ios/test/store_kit_wrappers/sk_methodchannel_apis_test.dart +++ b/packages/in_app_purchase/in_app_purchase_ios/test/store_kit_wrappers/sk_methodchannel_apis_test.dart @@ -23,6 +23,7 @@ void main() { tearDown(() { fakeIOSPlatform.testReturnNull = false; fakeIOSPlatform.queueIsActive = null; + fakeIOSPlatform.getReceiptFailTest = false; }); group('sk_request_maker', () { From e91f7e74428de53f599783b73d6937a2336d3afb Mon Sep 17 00:00:00 2001 From: Rene Floor Date: Mon, 28 Jun 2021 11:48:24 +0200 Subject: [PATCH 5/7] Improve code based on PR review --- .../RunnerTests/InAppPurchasePluginTests.m | 2 -- .../example/ios/RunnerTests/Stubs.h | 2 ++ .../example/ios/RunnerTests/Stubs.m | 1 + .../ios/Classes/FIAPReceiptManager.m | 29 ++++++++++++++----- 4 files changed, 25 insertions(+), 9 deletions(-) diff --git a/packages/in_app_purchase/in_app_purchase_ios/example/ios/RunnerTests/InAppPurchasePluginTests.m b/packages/in_app_purchase/in_app_purchase_ios/example/ios/RunnerTests/InAppPurchasePluginTests.m index 0e775d732283..92e3af80b0d0 100644 --- a/packages/in_app_purchase/in_app_purchase_ios/example/ios/RunnerTests/InAppPurchasePluginTests.m +++ b/packages/in_app_purchase/in_app_purchase_ios/example/ios/RunnerTests/InAppPurchasePluginTests.m @@ -232,7 +232,6 @@ - (void)testRetrieveReceiptDataSuccess { [expectation fulfill]; }]; [self waitForExpectations:@[ expectation ] timeout:5]; - NSLog(@"%@", result); XCTAssertNotNil(result); XCTAssert([result isKindOfClass:[NSString class]]); } @@ -250,7 +249,6 @@ - (void)testRetrieveReceiptDataError { [expectation fulfill]; }]; [self waitForExpectations:@[ expectation ] timeout:5]; - NSLog(@"%@", result); XCTAssertNotNil(result); XCTAssert([result isKindOfClass:[FlutterError class]]); } diff --git a/packages/in_app_purchase/in_app_purchase_ios/example/ios/RunnerTests/Stubs.h b/packages/in_app_purchase/in_app_purchase_ios/example/ios/RunnerTests/Stubs.h index df5d26627a37..80ae5150e1cf 100644 --- a/packages/in_app_purchase/in_app_purchase_ios/example/ios/RunnerTests/Stubs.h +++ b/packages/in_app_purchase/in_app_purchase_ios/example/ios/RunnerTests/Stubs.h @@ -54,6 +54,8 @@ API_AVAILABLE(ios(11.2), macos(10.13.2)) @end @interface FIAPReceiptManagerStub : FIAPReceiptManager +// Indicates whether getReceiptData of this stub is going to return an error. +// Setting this to true will let getReceiptData give a basic NSError and return nil. @property(assign, nonatomic) BOOL returnError; @end diff --git a/packages/in_app_purchase/in_app_purchase_ios/example/ios/RunnerTests/Stubs.m b/packages/in_app_purchase/in_app_purchase_ios/example/ios/RunnerTests/Stubs.m index 99ba99eb2ade..2d05d16b9fd7 100644 --- a/packages/in_app_purchase/in_app_purchase_ios/example/ios/RunnerTests/Stubs.m +++ b/packages/in_app_purchase/in_app_purchase_ios/example/ios/RunnerTests/Stubs.m @@ -262,6 +262,7 @@ @implementation FIAPReceiptManagerStub : FIAPReceiptManager - (NSData *)getReceiptData:(NSURL *)url error:(NSError **)error { if (self.returnError) { *error = [[NSError alloc] init]; + return nil; } NSString *originalString = [NSString stringWithFormat:@"test"]; return [[NSData alloc] initWithBase64EncodedString:originalString options:kNilOptions]; diff --git a/packages/in_app_purchase/in_app_purchase_ios/ios/Classes/FIAPReceiptManager.m b/packages/in_app_purchase/in_app_purchase_ios/ios/Classes/FIAPReceiptManager.m index ceee71af3c5c..1abbb2abfa57 100644 --- a/packages/in_app_purchase/in_app_purchase_ios/ios/Classes/FIAPReceiptManager.m +++ b/packages/in_app_purchase/in_app_purchase_ios/ios/Classes/FIAPReceiptManager.m @@ -5,19 +5,34 @@ #import "FIAPReceiptManager.h" #import +@interface FIAPReceiptManager () + +- (NSData *)getReceiptData:(NSURL *)url error:(NSError **)error; + +@end + @implementation FIAPReceiptManager -- (NSString *)retrieveReceiptWithError:(FlutterError **)error { +- (NSString *)retrieveReceiptWithError:(FlutterError **)flutterError { NSURL *receiptURL = [[NSBundle mainBundle] appStoreReceiptURL]; - NSError *err; - NSData *receipt = [self getReceiptData:receiptURL error:&err]; - if (err) { - *error = [FlutterError errorWithCode:[[NSString alloc] initWithFormat:@"%li", (long)err.code] - message:err.domain - details:err.userInfo]; + NSError *receiptError; + NSData *receipt = [self getReceiptData:receiptURL error:&receiptError]; + if (receiptError) { + if (flutterError != nil) { + *flutterError = [FlutterError + errorWithCode:[[NSString alloc] initWithFormat:@"%li", (long)receiptError.code] + message:receiptError.domain + details:receiptError.userInfo]; + } return nil; } if (!receipt) { + if (flutterError != nil) { + *flutterError = [FlutterError errorWithCode:@"0" + message:@"dataWithContentsOfURL returned nil without an " + @"error in retrieveReceiptWithError" + details:nil]; + } return nil; } return [receipt base64EncodedStringWithOptions:kNilOptions]; From 9ad91fde55510a2b8cbafda0ea9538f98d5e79bd Mon Sep 17 00:00:00 2001 From: Rene Floor Date: Mon, 28 Jun 2021 14:06:57 +0200 Subject: [PATCH 6/7] match style nil check --- .../in_app_purchase_ios/ios/Classes/FIAPReceiptManager.m | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/in_app_purchase/in_app_purchase_ios/ios/Classes/FIAPReceiptManager.m b/packages/in_app_purchase/in_app_purchase_ios/ios/Classes/FIAPReceiptManager.m index 1abbb2abfa57..2332da139b39 100644 --- a/packages/in_app_purchase/in_app_purchase_ios/ios/Classes/FIAPReceiptManager.m +++ b/packages/in_app_purchase/in_app_purchase_ios/ios/Classes/FIAPReceiptManager.m @@ -18,7 +18,7 @@ - (NSString *)retrieveReceiptWithError:(FlutterError **)flutterError { NSError *receiptError; NSData *receipt = [self getReceiptData:receiptURL error:&receiptError]; if (receiptError) { - if (flutterError != nil) { + if (flutterError) { *flutterError = [FlutterError errorWithCode:[[NSString alloc] initWithFormat:@"%li", (long)receiptError.code] message:receiptError.domain @@ -27,7 +27,7 @@ - (NSString *)retrieveReceiptWithError:(FlutterError **)flutterError { return nil; } if (!receipt) { - if (flutterError != nil) { + if (flutterError) { *flutterError = [FlutterError errorWithCode:@"0" message:@"dataWithContentsOfURL returned nil without an " @"error in retrieveReceiptWithError" From 097131f906b454803e72d36c859871851f39521b Mon Sep 17 00:00:00 2001 From: Rene Floor Date: Tue, 29 Jun 2021 11:02:58 +0200 Subject: [PATCH 7/7] added doc and changed null check --- .../ios/Classes/FIAPReceiptManager.m | 14 +++----------- 1 file changed, 3 insertions(+), 11 deletions(-) diff --git a/packages/in_app_purchase/in_app_purchase_ios/ios/Classes/FIAPReceiptManager.m b/packages/in_app_purchase/in_app_purchase_ios/ios/Classes/FIAPReceiptManager.m index 2332da139b39..8038304d178f 100644 --- a/packages/in_app_purchase/in_app_purchase_ios/ios/Classes/FIAPReceiptManager.m +++ b/packages/in_app_purchase/in_app_purchase_ios/ios/Classes/FIAPReceiptManager.m @@ -6,7 +6,8 @@ #import @interface FIAPReceiptManager () - +// Gets the receipt file data from the location of the url. Can be nil if +// there is an error. This interface is defined so it can be stubbed for testing. - (NSData *)getReceiptData:(NSURL *)url error:(NSError **)error; @end @@ -17,7 +18,7 @@ - (NSString *)retrieveReceiptWithError:(FlutterError **)flutterError { NSURL *receiptURL = [[NSBundle mainBundle] appStoreReceiptURL]; NSError *receiptError; NSData *receipt = [self getReceiptData:receiptURL error:&receiptError]; - if (receiptError) { + if (!receipt || receiptError) { if (flutterError) { *flutterError = [FlutterError errorWithCode:[[NSString alloc] initWithFormat:@"%li", (long)receiptError.code] @@ -26,15 +27,6 @@ - (NSString *)retrieveReceiptWithError:(FlutterError **)flutterError { } return nil; } - if (!receipt) { - if (flutterError) { - *flutterError = [FlutterError errorWithCode:@"0" - message:@"dataWithContentsOfURL returned nil without an " - @"error in retrieveReceiptWithError" - details:nil]; - } - return nil; - } return [receipt base64EncodedStringWithOptions:kNilOptions]; }