Skip to content

Commit 2aa2e5a

Browse files
authored
[in_app_purchase]Iap/ios add cancel status (flutter#4094)
1 parent e6ff352 commit 2aa2e5a

File tree

6 files changed

+99
-7
lines changed

6 files changed

+99
-7
lines changed

packages/in_app_purchase/in_app_purchase_ios/CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,8 @@
1+
## 0.2.0
2+
3+
* BREAKING CHANGE : Refactor to handle new `PurchaseStatus` named `canceled`. This means developers
4+
can distinguish between an error and user cancellation.
5+
16
## 0.1.4
27

38
* Require Dart SDK >= 2.14.

packages/in_app_purchase/in_app_purchase_ios/lib/src/store_kit_wrappers/enum_converters.dart

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,8 @@ class SKTransactionStatusConverter
3030
}
3131

3232
/// Converts an [SKPaymentTransactionStateWrapper] to a [PurchaseStatus].
33-
PurchaseStatus toPurchaseStatus(SKPaymentTransactionStateWrapper object) {
33+
PurchaseStatus toPurchaseStatus(
34+
SKPaymentTransactionStateWrapper object, SKError? error) {
3435
switch (object) {
3536
case SKPaymentTransactionStateWrapper.purchasing:
3637
case SKPaymentTransactionStateWrapper.deferred:
@@ -40,6 +41,14 @@ class SKTransactionStatusConverter
4041
case SKPaymentTransactionStateWrapper.restored:
4142
return PurchaseStatus.restored;
4243
case SKPaymentTransactionStateWrapper.failed:
44+
// According to the Apple documentation the error code "2" indicates
45+
// the user cancelled the payment (SKErrorPaymentCancelled) and error
46+
// code "15" indicates the cancellation of the overlay (SKErrorOverlayCancelled).
47+
// An overview of all error codes can be found at: https://developer.apple.com/documentation/storekit/skerrorcode?language=objc
48+
if (error != null && (error.code == 2 || error.code == 15)) {
49+
return PurchaseStatus.canceled;
50+
}
51+
return PurchaseStatus.error;
4352
case SKPaymentTransactionStateWrapper.unspecified:
4453
return PurchaseStatus.error;
4554
}

packages/in_app_purchase/in_app_purchase_ios/lib/src/types/app_store_purchase_details.dart

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ class AppStorePurchaseDetails extends PurchaseDetails {
5656
purchaseID: transaction.transactionIdentifier,
5757
skPaymentTransaction: transaction,
5858
status: SKTransactionStatusConverter()
59-
.toPurchaseStatus(transaction.transactionState),
59+
.toPurchaseStatus(transaction.transactionState, transaction.error),
6060
transactionDate: transaction.transactionTimeStamp != null
6161
? (transaction.transactionTimeStamp! * 1000).toInt().toString()
6262
: null,
@@ -66,7 +66,8 @@ class AppStorePurchaseDetails extends PurchaseDetails {
6666
source: kIAPSource),
6767
);
6868

69-
if (purchaseDetails.status == PurchaseStatus.error) {
69+
if (purchaseDetails.status == PurchaseStatus.error ||
70+
purchaseDetails.status == PurchaseStatus.canceled) {
7071
purchaseDetails.error = IAPError(
7172
source: kIAPSource,
7273
code: kPurchaseErrorCode,

packages/in_app_purchase/in_app_purchase_ios/pubspec.yaml

Lines changed: 2 additions & 2 deletions
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.4
5+
version: 0.2.0
66

77
environment:
88
sdk: ">=2.14.0 <3.0.0"
@@ -19,7 +19,7 @@ dependencies:
1919
collection: ^1.15.0
2020
flutter:
2121
sdk: flutter
22-
in_app_purchase_platform_interface: ^1.1.0
22+
in_app_purchase_platform_interface: ^1.3.0
2323
json_annotation: ^4.3.0
2424
meta: ^1.3.0
2525

packages/in_app_purchase/in_app_purchase_ios/test/fakes/fake_ios_platform.dart

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,14 +18,15 @@ class FakeIOSPlatform {
1818
channel.setMockMethodCallHandler(onMethodCall);
1919
}
2020

21-
// pre-configured store informations
21+
// pre-configured store information
2222
String? receiptData;
2323
late Set<String> validProductIDs;
2424
late Map<String, SKProductWrapper> validProducts;
2525
late List<SKPaymentTransactionWrapper> transactions;
2626
late List<SKPaymentTransactionWrapper> finishedTransactions;
2727
late bool testRestoredTransactionsNull;
2828
late bool testTransactionFail;
29+
late int testTransactionCancel;
2930
PlatformException? queryProductException;
3031
PlatformException? restoreException;
3132
SKError? testRestoredError;
@@ -67,6 +68,7 @@ class FakeIOSPlatform {
6768
finishedTransactions = [];
6869
testRestoredTransactionsNull = false;
6970
testTransactionFail = false;
71+
testTransactionCancel = -1;
7072
queryProductException = null;
7173
restoreException = null;
7274
testRestoredError = null;
@@ -107,6 +109,20 @@ class FakeIOSPlatform {
107109
originalTransaction: null);
108110
}
109111

112+
SKPaymentTransactionWrapper createCanceledTransaction(
113+
String productId, int errorCode) {
114+
return SKPaymentTransactionWrapper(
115+
transactionIdentifier: '',
116+
payment: SKPaymentWrapper(productIdentifier: productId),
117+
transactionState: SKPaymentTransactionStateWrapper.failed,
118+
transactionTimeStamp: 123123.121,
119+
error: SKError(
120+
code: errorCode,
121+
domain: 'ios_domain',
122+
userInfo: {'message': 'an error message'}),
123+
originalTransaction: null);
124+
}
125+
110126
Future<dynamic> onMethodCall(MethodCall call) {
111127
switch (call.method) {
112128
case '-[SKPaymentQueue canMakePayments:]':
@@ -167,6 +183,11 @@ class FakeIOSPlatform {
167183
createFailedTransaction(id);
168184
InAppPurchaseIosPlatform.observer
169185
.updatedTransactions(transactions: [transaction_failed]);
186+
} else if (testTransactionCancel > 0) {
187+
SKPaymentTransactionWrapper transaction_canceled =
188+
createCanceledTransaction(id, testTransactionCancel);
189+
InAppPurchaseIosPlatform.observer
190+
.updatedTransactions(transactions: [transaction_canceled]);
170191
} else {
171192
SKPaymentTransactionWrapper transaction_finished =
172193
createPurchasedTransaction(

packages/in_app_purchase/in_app_purchase_ios/test/in_app_purchase_ios_platform_test.dart

Lines changed: 57 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -130,7 +130,7 @@ void main() {
130130
expect(
131131
actual.status,
132132
SKTransactionStatusConverter()
133-
.toPurchaseStatus(expected.transactionState),
133+
.toPurchaseStatus(expected.transactionState, expected.error),
134134
);
135135
expect(actual.verificationData.localVerificationData,
136136
fakeIOSPlatform.receiptData);
@@ -275,6 +275,62 @@ void main() {
275275
expect(completerError.message, 'ios_domain');
276276
expect(completerError.details, {'message': 'an error message'});
277277
});
278+
279+
test(
280+
'should get canceled purchase status when error code is SKErrorPaymentCancelled',
281+
() async {
282+
fakeIOSPlatform.testTransactionCancel = 2;
283+
List<PurchaseDetails> details = [];
284+
Completer completer = Completer();
285+
286+
Stream<List<PurchaseDetails>> stream = iapIosPlatform.purchaseStream;
287+
late StreamSubscription subscription;
288+
subscription = stream.listen((purchaseDetailsList) {
289+
details.addAll(purchaseDetailsList);
290+
purchaseDetailsList.forEach((purchaseDetails) {
291+
if (purchaseDetails.status == PurchaseStatus.canceled) {
292+
completer.complete(purchaseDetails.status);
293+
subscription.cancel();
294+
}
295+
});
296+
});
297+
final AppStorePurchaseParam purchaseParam = AppStorePurchaseParam(
298+
productDetails:
299+
AppStoreProductDetails.fromSKProduct(dummyProductWrapper),
300+
applicationUserName: 'appName');
301+
await iapIosPlatform.buyNonConsumable(purchaseParam: purchaseParam);
302+
303+
PurchaseStatus purchaseStatus = await completer.future;
304+
expect(purchaseStatus, PurchaseStatus.canceled);
305+
});
306+
307+
test(
308+
'should get canceled purchase status when error code is SKErrorOverlayCancelled',
309+
() async {
310+
fakeIOSPlatform.testTransactionCancel = 15;
311+
List<PurchaseDetails> details = [];
312+
Completer completer = Completer();
313+
314+
Stream<List<PurchaseDetails>> stream = iapIosPlatform.purchaseStream;
315+
late StreamSubscription subscription;
316+
subscription = stream.listen((purchaseDetailsList) {
317+
details.addAll(purchaseDetailsList);
318+
purchaseDetailsList.forEach((purchaseDetails) {
319+
if (purchaseDetails.status == PurchaseStatus.canceled) {
320+
completer.complete(purchaseDetails.status);
321+
subscription.cancel();
322+
}
323+
});
324+
});
325+
final AppStorePurchaseParam purchaseParam = AppStorePurchaseParam(
326+
productDetails:
327+
AppStoreProductDetails.fromSKProduct(dummyProductWrapper),
328+
applicationUserName: 'appName');
329+
await iapIosPlatform.buyNonConsumable(purchaseParam: purchaseParam);
330+
331+
PurchaseStatus purchaseStatus = await completer.future;
332+
expect(purchaseStatus, PurchaseStatus.canceled);
333+
});
278334
});
279335

280336
group('complete purchase', () {

0 commit comments

Comments
 (0)