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 c4c4eb05cecd..f6acc2b6d6ce 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.1+1 + +* iOS: Fix treating missing App Store receipt as an exception. + ## 0.1.1 * Added support to register a `SKPaymentQueueDelegateWrapper` and handle changes to active subscriptions accordingly (see also Store Kit's [SKPaymentQueueDelegate](https://developer.apple.com/documentation/storekit/skpaymentqueuedelegate?language=objc)). @@ -13,4 +17,4 @@ ## 0.1.0 -* Initial open-source release. \ No newline at end of file +* Initial open-source release. 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 045abcdea922..e259e69d962c 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:]" @@ -231,8 +232,25 @@ - (void)testRetrieveReceiptData { [expectation fulfill]; }]; [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]; + 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 7b6842da4c77..085a06337386 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,9 @@ 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 @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 a57831c61da5..f247a7e4b78a 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,11 @@ - (instancetype)initWithMap:(NSDictionary *)map { @implementation FIAPReceiptManagerStub : FIAPReceiptManager -- (NSData *)getReceiptData:(NSURL *)url { +- (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 526364020ad3..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 @@ -5,22 +5,33 @@ #import "FIAPReceiptManager.h" #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 + @implementation FIAPReceiptManager -- (NSString *)retrieveReceiptWithError:(FlutterError **)error { +- (NSString *)retrieveReceiptWithError:(FlutterError **)flutterError { NSURL *receiptURL = [[NSBundle mainBundle] appStoreReceiptURL]; - NSData *receipt = [self getReceiptData:receiptURL]; - if (!receipt) { - *error = [FlutterError errorWithCode:@"storekit_no_receipt" - message:@"Cannot find receipt for the current main bundle." - details:nil]; + NSError *receiptError; + NSData *receipt = [self getReceiptData:receiptURL error:&receiptError]; + if (!receipt || receiptError) { + if (flutterError) { + *flutterError = [FlutterError + errorWithCode:[[NSString alloc] initWithFormat:@"%li", (long)receiptError.code] + message:receiptError.domain + details:receiptError.userInfo]; + } 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 bcc4ddf48200..359e51713521 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,14 +21,18 @@ 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); } /// Sets an implementation of the [SKPaymentQueueDelegateWrapper]. 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 00929d9c024b..d06e5fef0bd4 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.1 +version: 0.1.1+1 environment: sdk: ">=2.12.0 <3.0.0" 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 6a01fe4caecb..7bfddaa0a32a 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', () { @@ -74,6 +75,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', () { @@ -180,6 +187,9 @@ class FakeIOSPlatform { bool getProductRequestFailTest = false; bool testReturnNull = false; + // get receipt request + bool getReceiptFailTest = false; + // refresh receipt request int refreshReceipt = 0; late Map refreshReceiptParam; @@ -221,6 +231,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:]':