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

Commit 245750c

Browse files
committed
Added support for iOS specific features
1 parent 61dc0be commit 245750c

File tree

6 files changed

+257
-171
lines changed

6 files changed

+257
-171
lines changed

packages/in_app_purchase/in_app_purchase_ios/lib/in_app_purchase_ios.dart

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,4 +3,5 @@
33
// found in the LICENSE file.
44

55
export 'src/in_app_purchase_ios_platform.dart';
6+
export 'src/in_app_purchase_ios_platform_addition.dart';
67
export 'src/types/types.dart';

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

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import 'dart:async';
66

77
import 'package:flutter/foundation.dart';
88
import 'package:flutter/services.dart';
9+
import 'package:in_app_purchase_ios/src/in_app_purchase_ios_platform_addition.dart';
910
import 'package:in_app_purchase_platform_interface/in_app_purchase_platform_interface.dart';
1011

1112
import '../in_app_purchase_ios.dart';
@@ -27,7 +28,7 @@ final String kIAPSource = 'app_store';
2728
///
2829
/// This translates various `StoreKit` calls and responses into the
2930
/// generic plugin API.
30-
class InAppPurchaseIosPlatform implements InAppPurchasePlatform {
31+
class InAppPurchaseIosPlatform extends InAppPurchasePlatform {
3132
/// Returns the singleton instance of the [InAppPurchaseIosPlatform] that should be
3233
/// used across the app.
3334
static InAppPurchaseIosPlatform get instance => _getOrCreateInstance();
@@ -54,6 +55,12 @@ class InAppPurchaseIosPlatform implements InAppPurchasePlatform {
5455
return _instance!;
5556
}
5657

58+
// Register the [InAppPurchaseIosPlatformAddition] containing iOS
59+
// platform-specific functionality.
60+
InAppPurchasePlatformAddition.instance = InAppPurchaseIosPlatformAddition();
61+
62+
// Register the platform-specific implementation of the idiomatic
63+
// InAppPurchase API.
5764
_instance = InAppPurchaseIosPlatform();
5865
_skPaymentQueueWrapper = SKPaymentQueueWrapper();
5966
_observer = _TransactionObserver(StreamController.broadcast());
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import 'package:in_app_purchase_ios/in_app_purchase_ios.dart';
2+
import 'package:in_app_purchase_platform_interface/in_app_purchase_platform_interface.dart';
3+
4+
import '../store_kit_wrappers.dart';
5+
6+
/// Contains InApp Purchase features that are only available on iOS.
7+
class InAppPurchaseIosPlatformAddition extends InAppPurchasePlatformAddition {
8+
/// Present Code Redemption Sheet.
9+
///
10+
/// Available on devices running iOS 14 and iPadOS 14 and later.
11+
Future presentCodeRedemptionSheet() {
12+
return SKPaymentQueueWrapper().presentCodeRedemptionSheet();
13+
}
14+
15+
/// Retry loading purchase data after an initial failure.
16+
///
17+
/// If no results, a `null` value is returned.
18+
Future<PurchaseVerificationData?> refreshPurchaseVerificationData() async {
19+
await SKRequestMaker().startRefreshReceiptRequest();
20+
final String? receipt = await SKReceiptManager.retrieveReceiptData();
21+
if (receipt == null) {
22+
return null;
23+
}
24+
return PurchaseVerificationData(
25+
localVerificationData: receipt,
26+
serverVerificationData: receipt,
27+
source: kIAPSource);
28+
}
29+
}
Lines changed: 182 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,182 @@
1+
// Copyright 2013 The Flutter Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style license that can be
3+
// found in the LICENSE file.
4+
5+
import 'dart:async';
6+
import 'dart:io';
7+
8+
import 'package:flutter/services.dart';
9+
import 'package:flutter_test/flutter_test.dart';
10+
import 'package:in_app_purchase_ios/in_app_purchase_ios.dart';
11+
import 'package:in_app_purchase_ios/src/channel.dart';
12+
import 'package:in_app_purchase_ios/store_kit_wrappers.dart';
13+
14+
import '../store_kit_wrappers/sk_test_stub_objects.dart';
15+
16+
class FakeIOSPlatform {
17+
FakeIOSPlatform() {
18+
channel.setMockMethodCallHandler(onMethodCall);
19+
}
20+
21+
// pre-configured store informations
22+
String? receiptData;
23+
late Set<String> validProductIDs;
24+
late Map<String, SKProductWrapper> validProducts;
25+
late List<SKPaymentTransactionWrapper> transactions;
26+
late List<SKPaymentTransactionWrapper> finishedTransactions;
27+
late bool testRestoredTransactionsNull;
28+
late bool testTransactionFail;
29+
PlatformException? queryProductException;
30+
PlatformException? restoreException;
31+
SKError? testRestoredError;
32+
33+
void reset() {
34+
transactions = [];
35+
receiptData = 'dummy base64data';
36+
validProductIDs = ['123', '456'].toSet();
37+
validProducts = Map();
38+
for (String validID in validProductIDs) {
39+
Map<String, dynamic> productWrapperMap =
40+
buildProductMap(dummyProductWrapper);
41+
productWrapperMap['productIdentifier'] = validID;
42+
validProducts[validID] = SKProductWrapper.fromJson(productWrapperMap);
43+
}
44+
45+
SKPaymentTransactionWrapper tran1 = SKPaymentTransactionWrapper(
46+
transactionIdentifier: '123',
47+
payment: dummyPayment,
48+
originalTransaction: dummyTransaction,
49+
transactionTimeStamp: 123123123.022,
50+
transactionState: SKPaymentTransactionStateWrapper.restored,
51+
error: null,
52+
);
53+
SKPaymentTransactionWrapper tran2 = SKPaymentTransactionWrapper(
54+
transactionIdentifier: '1234',
55+
payment: dummyPayment,
56+
originalTransaction: dummyTransaction,
57+
transactionTimeStamp: 123123123.022,
58+
transactionState: SKPaymentTransactionStateWrapper.restored,
59+
error: null,
60+
);
61+
62+
transactions.addAll([tran1, tran2]);
63+
finishedTransactions = [];
64+
testRestoredTransactionsNull = false;
65+
testTransactionFail = false;
66+
queryProductException = null;
67+
restoreException = null;
68+
testRestoredError = null;
69+
}
70+
71+
SKPaymentTransactionWrapper createPendingTransaction(String id) {
72+
return SKPaymentTransactionWrapper(
73+
transactionIdentifier: '',
74+
payment: SKPaymentWrapper(productIdentifier: id),
75+
transactionState: SKPaymentTransactionStateWrapper.purchasing,
76+
transactionTimeStamp: 123123.121,
77+
error: null,
78+
originalTransaction: null);
79+
}
80+
81+
SKPaymentTransactionWrapper createPurchasedTransaction(
82+
String productId, String transactionId) {
83+
return SKPaymentTransactionWrapper(
84+
payment: SKPaymentWrapper(productIdentifier: productId),
85+
transactionState: SKPaymentTransactionStateWrapper.purchased,
86+
transactionTimeStamp: 123123.121,
87+
transactionIdentifier: transactionId,
88+
error: null,
89+
originalTransaction: null);
90+
}
91+
92+
SKPaymentTransactionWrapper createFailedTransaction(String productId) {
93+
return SKPaymentTransactionWrapper(
94+
transactionIdentifier: '',
95+
payment: SKPaymentWrapper(productIdentifier: productId),
96+
transactionState: SKPaymentTransactionStateWrapper.failed,
97+
transactionTimeStamp: 123123.121,
98+
error: SKError(
99+
code: 0,
100+
domain: 'ios_domain',
101+
userInfo: {'message': 'an error message'}),
102+
originalTransaction: null);
103+
}
104+
105+
Future<dynamic> onMethodCall(MethodCall call) {
106+
switch (call.method) {
107+
case '-[SKPaymentQueue canMakePayments:]':
108+
return Future<bool>.value(true);
109+
case '-[InAppPurchasePlugin startProductRequest:result:]':
110+
if (queryProductException != null) {
111+
throw queryProductException!;
112+
}
113+
List<String> productIDS =
114+
List.castFrom<dynamic, String>(call.arguments);
115+
assert(productIDS is List<String>, 'invalid argument type');
116+
List<String> invalidFound = [];
117+
List<SKProductWrapper> products = [];
118+
for (String productID in productIDS) {
119+
if (!validProductIDs.contains(productID)) {
120+
invalidFound.add(productID);
121+
} else {
122+
products.add(validProducts[productID]!);
123+
}
124+
}
125+
SkProductResponseWrapper response = SkProductResponseWrapper(
126+
products: products, invalidProductIdentifiers: invalidFound);
127+
return Future<Map<String, dynamic>>.value(
128+
buildProductResponseMap(response));
129+
case '-[InAppPurchasePlugin restoreTransactions:result:]':
130+
if (restoreException != null) {
131+
throw restoreException!;
132+
}
133+
if (testRestoredError != null) {
134+
InAppPurchaseIosPlatform.observer
135+
.restoreCompletedTransactionsFailed(error: testRestoredError!);
136+
return Future<void>.sync(() {});
137+
}
138+
if (!testRestoredTransactionsNull) {
139+
InAppPurchaseIosPlatform.observer
140+
.updatedTransactions(transactions: transactions);
141+
}
142+
InAppPurchaseIosPlatform.observer
143+
.paymentQueueRestoreCompletedTransactionsFinished();
144+
145+
return Future<void>.sync(() {});
146+
case '-[InAppPurchasePlugin retrieveReceiptData:result:]':
147+
if (receiptData != null) {
148+
return Future<void>.value(receiptData);
149+
} else {
150+
throw PlatformException(code: 'no_receipt_data');
151+
}
152+
case '-[InAppPurchasePlugin refreshReceipt:result:]':
153+
receiptData = 'refreshed receipt data';
154+
return Future<void>.sync(() {});
155+
case '-[InAppPurchasePlugin addPayment:result:]':
156+
String id = call.arguments['productIdentifier'];
157+
SKPaymentTransactionWrapper transaction = createPendingTransaction(id);
158+
InAppPurchaseIosPlatform.observer
159+
.updatedTransactions(transactions: [transaction]);
160+
sleep(const Duration(milliseconds: 30));
161+
if (testTransactionFail) {
162+
SKPaymentTransactionWrapper transaction_failed =
163+
createFailedTransaction(id);
164+
InAppPurchaseIosPlatform.observer
165+
.updatedTransactions(transactions: [transaction_failed]);
166+
} else {
167+
SKPaymentTransactionWrapper transaction_finished =
168+
createPurchasedTransaction(
169+
id, transaction.transactionIdentifier ?? '');
170+
InAppPurchaseIosPlatform.observer
171+
.updatedTransactions(transactions: [transaction_finished]);
172+
}
173+
break;
174+
case '-[InAppPurchasePlugin finishTransaction:result:]':
175+
finishedTransactions.add(createPurchasedTransaction(
176+
call.arguments["productIdentifier"],
177+
call.arguments["transactionIdentifier"]));
178+
break;
179+
}
180+
return Future<void>.sync(() {});
181+
}
182+
}
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import 'package:flutter/services.dart';
2+
import 'package:flutter_test/flutter_test.dart';
3+
import 'package:in_app_purchase_ios/in_app_purchase_ios.dart';
4+
import 'package:in_app_purchase_platform_interface/in_app_purchase_platform_interface.dart';
5+
6+
import 'fakes/fake_ios_platform.dart';
7+
8+
void main() {
9+
TestWidgetsFlutterBinding.ensureInitialized();
10+
11+
final FakeIOSPlatform fakeIOSPlatform = FakeIOSPlatform();
12+
13+
setUpAll(() {
14+
SystemChannels.platform
15+
.setMockMethodCallHandler(fakeIOSPlatform.onMethodCall);
16+
});
17+
18+
group('present code redemption sheet', () {
19+
test('null', () async {
20+
expect(
21+
await InAppPurchaseIosPlatformAddition().presentCodeRedemptionSheet(), null);
22+
});
23+
});
24+
25+
group('refresh receipt data', () {
26+
test('should refresh receipt data', () async {
27+
PurchaseVerificationData? receiptData =
28+
await InAppPurchaseIosPlatformAddition()
29+
.refreshPurchaseVerificationData();
30+
expect(receiptData, isNotNull);
31+
expect(receiptData!.source, kIAPSource);
32+
expect(receiptData.localVerificationData, 'refreshed receipt data');
33+
expect(receiptData.serverVerificationData, 'refreshed receipt data');
34+
});
35+
});
36+
}

0 commit comments

Comments
 (0)