Skip to content
This repository was archived by the owner on Feb 22, 2023. It is now read-only.

[in_app_purchase] Fix app exceptions caused by missing App Store receipt #4096

Merged
merged 8 commits into from
Jun 30, 2021
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
6 changes: 5 additions & 1 deletion packages/in_app_purchase/in_app_purchase_ios/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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)).
Expand All @@ -13,4 +17,4 @@

## 0.1.0

* Initial open-source release.
* Initial open-source release.
Original file line number Diff line number Diff line change
Expand Up @@ -11,15 +11,16 @@

@interface InAppPurchasePluginTest : XCTestCase

@property(strong, nonatomic) FIAPReceiptManagerStub* receiptManagerStub;
@property(strong, nonatomic) InAppPurchasePlugin* plugin;

@end

@implementation InAppPurchasePluginTest

- (void)setUp {
self.plugin =
[[InAppPurchasePluginStub alloc] initWithReceiptManager:[FIAPReceiptManagerStub new]];
self.receiptManagerStub = [FIAPReceiptManagerStub new];
self.plugin = [[InAppPurchasePluginStub alloc] initWithReceiptManager:self.receiptManagerStub];
}

- (void)tearDown {
Expand Down Expand Up @@ -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:]"
Expand All @@ -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 {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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];
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,22 +5,33 @@
#import "FIAPReceiptManager.h"
#import <Flutter/Flutter.h>

@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
Original file line number Diff line number Diff line change
Expand Up @@ -21,14 +21,18 @@ class InAppPurchaseIosPlatformAddition extends InAppPurchasePlatformAddition {
/// If no results, a `null` value is returned.
Future<PurchaseVerificationData?> 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].
Expand Down
2 changes: 1 addition & 1 deletion packages/in_app_purchase/in_app_purchase_ios/pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ void main() {
tearDown(() {
fakeIOSPlatform.testReturnNull = false;
fakeIOSPlatform.queueIsActive = null;
fakeIOSPlatform.getReceiptFailTest = false;
});

group('sk_request_maker', () {
Expand Down Expand Up @@ -74,6 +75,12 @@ void main() {
expect(fakeIOSPlatform.refreshReceiptParam,
<String, dynamic>{"isExpired": true});
});

test('should get null receipt if any exceptions are raised', () async {
fakeIOSPlatform.getReceiptFailTest = true;
expect(() async => SKReceiptManager.retrieveReceiptData(),
throwsA(TypeMatcher<PlatformException>()));
});
});

group('sk_receipt_manager', () {
Expand Down Expand Up @@ -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<String, dynamic> refreshReceiptParam;
Expand Down Expand Up @@ -221,6 +231,9 @@ class FakeIOSPlatform {
return Future<void>.sync(() {});
// receipt manager
case '-[InAppPurchasePlugin retrieveReceiptData:result:]':
if (getReceiptFailTest) {
throw ("some arbitrary error");
}
return Future<String>.value('receipt data');
// payment queue
case '-[SKPaymentQueue canMakePayments:]':
Expand Down