Skip to content

Commit acc502e

Browse files
authored
[in_app_purchase] Fix app exceptions caused by missing App Store receipt (flutter#4096)
1 parent 94caa8a commit acc502e

File tree

8 files changed

+78
-21
lines changed

8 files changed

+78
-21
lines changed

packages/in_app_purchase/in_app_purchase_ios/CHANGELOG.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
## 0.1.1+1
2+
3+
* iOS: Fix treating missing App Store receipt as an exception.
4+
15
## 0.1.1
26

37
* 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 @@
1317

1418
## 0.1.0
1519

16-
* Initial open-source release.
20+
* Initial open-source release.

packages/in_app_purchase/in_app_purchase_ios/example/ios/RunnerTests/InAppPurchasePluginTests.m

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,15 +11,16 @@
1111

1212
@interface InAppPurchasePluginTest : XCTestCase
1313

14+
@property(strong, nonatomic) FIAPReceiptManagerStub* receiptManagerStub;
1415
@property(strong, nonatomic) InAppPurchasePlugin* plugin;
1516

1617
@end
1718

1819
@implementation InAppPurchasePluginTest
1920

2021
- (void)setUp {
21-
self.plugin =
22-
[[InAppPurchasePluginStub alloc] initWithReceiptManager:[FIAPReceiptManagerStub new]];
22+
self.receiptManagerStub = [FIAPReceiptManagerStub new];
23+
self.plugin = [[InAppPurchasePluginStub alloc] initWithReceiptManager:self.receiptManagerStub];
2324
}
2425

2526
- (void)tearDown {
@@ -219,7 +220,7 @@ - (void)testRestoreTransactions {
219220
XCTAssertTrue(callbackInvoked);
220221
}
221222

222-
- (void)testRetrieveReceiptData {
223+
- (void)testRetrieveReceiptDataSuccess {
223224
XCTestExpectation* expectation = [self expectationWithDescription:@"receipt data retrieved"];
224225
FlutterMethodCall* call = [FlutterMethodCall
225226
methodCallWithMethodName:@"-[InAppPurchasePlugin retrieveReceiptData:result:]"
@@ -231,8 +232,25 @@ - (void)testRetrieveReceiptData {
231232
[expectation fulfill];
232233
}];
233234
[self waitForExpectations:@[ expectation ] timeout:5];
234-
NSLog(@"%@", result);
235235
XCTAssertNotNil(result);
236+
XCTAssert([result isKindOfClass:[NSString class]]);
237+
}
238+
239+
- (void)testRetrieveReceiptDataError {
240+
XCTestExpectation* expectation = [self expectationWithDescription:@"receipt data retrieved"];
241+
FlutterMethodCall* call = [FlutterMethodCall
242+
methodCallWithMethodName:@"-[InAppPurchasePlugin retrieveReceiptData:result:]"
243+
arguments:nil];
244+
__block NSDictionary* result;
245+
self.receiptManagerStub.returnError = YES;
246+
[self.plugin handleMethodCall:call
247+
result:^(id r) {
248+
result = r;
249+
[expectation fulfill];
250+
}];
251+
[self waitForExpectations:@[ expectation ] timeout:5];
252+
XCTAssertNotNil(result);
253+
XCTAssert([result isKindOfClass:[FlutterError class]]);
236254
}
237255

238256
- (void)testRefreshReceiptRequest {

packages/in_app_purchase/in_app_purchase_ios/example/ios/RunnerTests/Stubs.h

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,9 @@ API_AVAILABLE(ios(11.2), macos(10.13.2))
5454
@end
5555

5656
@interface FIAPReceiptManagerStub : FIAPReceiptManager
57+
// Indicates whether getReceiptData of this stub is going to return an error.
58+
// Setting this to true will let getReceiptData give a basic NSError and return nil.
59+
@property(assign, nonatomic) BOOL returnError;
5760
@end
5861

5962
@interface SKReceiptRefreshRequestStub : SKReceiptRefreshRequest

packages/in_app_purchase/in_app_purchase_ios/example/ios/RunnerTests/Stubs.m

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -259,7 +259,11 @@ - (instancetype)initWithMap:(NSDictionary *)map {
259259

260260
@implementation FIAPReceiptManagerStub : FIAPReceiptManager
261261

262-
- (NSData *)getReceiptData:(NSURL *)url {
262+
- (NSData *)getReceiptData:(NSURL *)url error:(NSError **)error {
263+
if (self.returnError) {
264+
*error = [[NSError alloc] init];
265+
return nil;
266+
}
263267
NSString *originalString = [NSString stringWithFormat:@"test"];
264268
return [[NSData alloc] initWithBase64EncodedString:originalString options:kNilOptions];
265269
}

packages/in_app_purchase/in_app_purchase_ios/ios/Classes/FIAPReceiptManager.m

Lines changed: 19 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5,22 +5,33 @@
55
#import "FIAPReceiptManager.h"
66
#import <Flutter/Flutter.h>
77

8+
@interface FIAPReceiptManager ()
9+
// Gets the receipt file data from the location of the url. Can be nil if
10+
// there is an error. This interface is defined so it can be stubbed for testing.
11+
- (NSData *)getReceiptData:(NSURL *)url error:(NSError **)error;
12+
13+
@end
14+
815
@implementation FIAPReceiptManager
916

10-
- (NSString *)retrieveReceiptWithError:(FlutterError **)error {
17+
- (NSString *)retrieveReceiptWithError:(FlutterError **)flutterError {
1118
NSURL *receiptURL = [[NSBundle mainBundle] appStoreReceiptURL];
12-
NSData *receipt = [self getReceiptData:receiptURL];
13-
if (!receipt) {
14-
*error = [FlutterError errorWithCode:@"storekit_no_receipt"
15-
message:@"Cannot find receipt for the current main bundle."
16-
details:nil];
19+
NSError *receiptError;
20+
NSData *receipt = [self getReceiptData:receiptURL error:&receiptError];
21+
if (!receipt || receiptError) {
22+
if (flutterError) {
23+
*flutterError = [FlutterError
24+
errorWithCode:[[NSString alloc] initWithFormat:@"%li", (long)receiptError.code]
25+
message:receiptError.domain
26+
details:receiptError.userInfo];
27+
}
1728
return nil;
1829
}
1930
return [receipt base64EncodedStringWithOptions:kNilOptions];
2031
}
2132

22-
- (NSData *)getReceiptData:(NSURL *)url {
23-
return [NSData dataWithContentsOfURL:url];
33+
- (NSData *)getReceiptData:(NSURL *)url error:(NSError **)error {
34+
return [NSData dataWithContentsOfURL:url options:NSDataReadingMappedIfSafe error:error];
2435
}
2536

2637
@end

packages/in_app_purchase/in_app_purchase_ios/lib/src/in_app_purchase_ios_platform_addition.dart

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -21,14 +21,18 @@ class InAppPurchaseIosPlatformAddition extends InAppPurchasePlatformAddition {
2121
/// If no results, a `null` value is returned.
2222
Future<PurchaseVerificationData?> refreshPurchaseVerificationData() async {
2323
await SKRequestMaker().startRefreshReceiptRequest();
24-
final String? receipt = await SKReceiptManager.retrieveReceiptData();
25-
if (receipt == null) {
24+
try {
25+
String receipt = await SKReceiptManager.retrieveReceiptData();
26+
return PurchaseVerificationData(
27+
localVerificationData: receipt,
28+
serverVerificationData: receipt,
29+
source: kIAPSource);
30+
} catch (e) {
31+
print(
32+
'Something is wrong while fetching the receipt, this normally happens when the app is '
33+
'running on a simulator: $e');
2634
return null;
2735
}
28-
return PurchaseVerificationData(
29-
localVerificationData: receipt,
30-
serverVerificationData: receipt,
31-
source: kIAPSource);
3236
}
3337

3438
/// Sets an implementation of the [SKPaymentQueueDelegateWrapper].

packages/in_app_purchase/in_app_purchase_ios/pubspec.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ name: in_app_purchase_ios
22
description: An implementation for the iOS platform of the Flutter `in_app_purchase` plugin. This uses the iOS StoreKit Framework.
33
repository: https://github.com/flutter/plugins/tree/master/packages/in_app_purchase/in_app_purchase_ios
44
issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+in_app_purchase%22
5-
version: 0.1.1
5+
version: 0.1.1+1
66

77
environment:
88
sdk: ">=2.12.0 <3.0.0"

packages/in_app_purchase/in_app_purchase_ios/test/store_kit_wrappers/sk_methodchannel_apis_test.dart

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ void main() {
2323
tearDown(() {
2424
fakeIOSPlatform.testReturnNull = false;
2525
fakeIOSPlatform.queueIsActive = null;
26+
fakeIOSPlatform.getReceiptFailTest = false;
2627
});
2728

2829
group('sk_request_maker', () {
@@ -74,6 +75,12 @@ void main() {
7475
expect(fakeIOSPlatform.refreshReceiptParam,
7576
<String, dynamic>{"isExpired": true});
7677
});
78+
79+
test('should get null receipt if any exceptions are raised', () async {
80+
fakeIOSPlatform.getReceiptFailTest = true;
81+
expect(() async => SKReceiptManager.retrieveReceiptData(),
82+
throwsA(TypeMatcher<PlatformException>()));
83+
});
7784
});
7885

7986
group('sk_receipt_manager', () {
@@ -180,6 +187,9 @@ class FakeIOSPlatform {
180187
bool getProductRequestFailTest = false;
181188
bool testReturnNull = false;
182189

190+
// get receipt request
191+
bool getReceiptFailTest = false;
192+
183193
// refresh receipt request
184194
int refreshReceipt = 0;
185195
late Map<String, dynamic> refreshReceiptParam;
@@ -221,6 +231,9 @@ class FakeIOSPlatform {
221231
return Future<void>.sync(() {});
222232
// receipt manager
223233
case '-[InAppPurchasePlugin retrieveReceiptData:result:]':
234+
if (getReceiptFailTest) {
235+
throw ("some arbitrary error");
236+
}
224237
return Future<String>.value('receipt data');
225238
// payment queue
226239
case '-[SKPaymentQueue canMakePayments:]':

0 commit comments

Comments
 (0)