Thanks to visit codestin.com
Credit goes to github.com

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

[in_app_purchase]Iap/ios add cancel status #4094

Merged
merged 11 commits into from
Nov 17, 2021
5 changes: 5 additions & 0 deletions packages/in_app_purchase/in_app_purchase_ios/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
## 0.2.0

* BREAKING CHANGE : Refactor to handle new `PurchaseStatus` named `canceled`. This means developers
can distinguish between an error and user cancellation.

## 0.1.4

* Require Dart SDK >= 2.14.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,8 @@ class SKTransactionStatusConverter
}

/// Converts an [SKPaymentTransactionStateWrapper] to a [PurchaseStatus].
PurchaseStatus toPurchaseStatus(SKPaymentTransactionStateWrapper object) {
PurchaseStatus toPurchaseStatus(
SKPaymentTransactionStateWrapper object, SKError? error) {
switch (object) {
case SKPaymentTransactionStateWrapper.purchasing:
case SKPaymentTransactionStateWrapper.deferred:
Expand All @@ -40,6 +41,14 @@ class SKTransactionStatusConverter
case SKPaymentTransactionStateWrapper.restored:
return PurchaseStatus.restored;
case SKPaymentTransactionStateWrapper.failed:
// According to the Apple documentation the error code "2" indicates
// the user cancelled the payment (SKErrorPaymentCancelled) and error
// code "15" indicates the cancellation of the overlay (SKErrorOverlayCancelled).
// An overview of all error codes can be found at: https://developer.apple.com/documentation/storekit/skerrorcode?language=objc
if (error != null && (error.code == 2 || error.code == 15)) {
return PurchaseStatus.canceled;
}
return PurchaseStatus.error;
case SKPaymentTransactionStateWrapper.unspecified:
return PurchaseStatus.error;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ class AppStorePurchaseDetails extends PurchaseDetails {
purchaseID: transaction.transactionIdentifier,
skPaymentTransaction: transaction,
status: SKTransactionStatusConverter()
.toPurchaseStatus(transaction.transactionState),
.toPurchaseStatus(transaction.transactionState, transaction.error),
transactionDate: transaction.transactionTimeStamp != null
? (transaction.transactionTimeStamp! * 1000).toInt().toString()
: null,
Expand All @@ -66,7 +66,8 @@ class AppStorePurchaseDetails extends PurchaseDetails {
source: kIAPSource),
);

if (purchaseDetails.status == PurchaseStatus.error) {
if (purchaseDetails.status == PurchaseStatus.error ||
purchaseDetails.status == PurchaseStatus.canceled) {
purchaseDetails.error = IAPError(
source: kIAPSource,
code: kPurchaseErrorCode,
Expand Down
4 changes: 2 additions & 2 deletions 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.4
version: 0.2.0

environment:
sdk: ">=2.14.0 <3.0.0"
Expand All @@ -19,7 +19,7 @@ dependencies:
collection: ^1.15.0
flutter:
sdk: flutter
in_app_purchase_platform_interface: ^1.1.0
in_app_purchase_platform_interface: ^1.3.0
json_annotation: ^4.3.0
meta: ^1.3.0

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,14 +18,15 @@ class FakeIOSPlatform {
channel.setMockMethodCallHandler(onMethodCall);
}

// pre-configured store informations
// pre-configured store information
String? receiptData;
late Set<String> validProductIDs;
late Map<String, SKProductWrapper> validProducts;
late List<SKPaymentTransactionWrapper> transactions;
late List<SKPaymentTransactionWrapper> finishedTransactions;
late bool testRestoredTransactionsNull;
late bool testTransactionFail;
late int testTransactionCancel;
PlatformException? queryProductException;
PlatformException? restoreException;
SKError? testRestoredError;
Expand Down Expand Up @@ -67,6 +68,7 @@ class FakeIOSPlatform {
finishedTransactions = [];
testRestoredTransactionsNull = false;
testTransactionFail = false;
testTransactionCancel = -1;
queryProductException = null;
restoreException = null;
testRestoredError = null;
Expand Down Expand Up @@ -107,6 +109,20 @@ class FakeIOSPlatform {
originalTransaction: null);
}

SKPaymentTransactionWrapper createCanceledTransaction(
String productId, int errorCode) {
return SKPaymentTransactionWrapper(
transactionIdentifier: '',
payment: SKPaymentWrapper(productIdentifier: productId),
transactionState: SKPaymentTransactionStateWrapper.failed,
transactionTimeStamp: 123123.121,
error: SKError(
code: errorCode,
domain: 'ios_domain',
userInfo: {'message': 'an error message'}),
originalTransaction: null);
}

Future<dynamic> onMethodCall(MethodCall call) {
switch (call.method) {
case '-[SKPaymentQueue canMakePayments:]':
Expand Down Expand Up @@ -167,6 +183,11 @@ class FakeIOSPlatform {
createFailedTransaction(id);
InAppPurchaseIosPlatform.observer
.updatedTransactions(transactions: [transaction_failed]);
} else if (testTransactionCancel > 0) {
SKPaymentTransactionWrapper transaction_canceled =
createCanceledTransaction(id, testTransactionCancel);
InAppPurchaseIosPlatform.observer
.updatedTransactions(transactions: [transaction_canceled]);
} else {
SKPaymentTransactionWrapper transaction_finished =
createPurchasedTransaction(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,7 @@ void main() {
expect(
actual.status,
SKTransactionStatusConverter()
.toPurchaseStatus(expected.transactionState),
.toPurchaseStatus(expected.transactionState, expected.error),
);
expect(actual.verificationData.localVerificationData,
fakeIOSPlatform.receiptData);
Expand Down Expand Up @@ -275,6 +275,62 @@ void main() {
expect(completerError.message, 'ios_domain');
expect(completerError.details, {'message': 'an error message'});
});

test(
'should get canceled purchase status when error code is SKErrorPaymentCancelled',
() async {
fakeIOSPlatform.testTransactionCancel = 2;
List<PurchaseDetails> details = [];
Completer completer = Completer();

Stream<List<PurchaseDetails>> stream = iapIosPlatform.purchaseStream;
late StreamSubscription subscription;
subscription = stream.listen((purchaseDetailsList) {
details.addAll(purchaseDetailsList);
purchaseDetailsList.forEach((purchaseDetails) {
if (purchaseDetails.status == PurchaseStatus.canceled) {
completer.complete(purchaseDetails.status);
subscription.cancel();
}
});
});
final AppStorePurchaseParam purchaseParam = AppStorePurchaseParam(
productDetails:
AppStoreProductDetails.fromSKProduct(dummyProductWrapper),
applicationUserName: 'appName');
await iapIosPlatform.buyNonConsumable(purchaseParam: purchaseParam);

PurchaseStatus purchaseStatus = await completer.future;
expect(purchaseStatus, PurchaseStatus.canceled);
});

test(
'should get canceled purchase status when error code is SKErrorOverlayCancelled',
() async {
fakeIOSPlatform.testTransactionCancel = 15;
List<PurchaseDetails> details = [];
Completer completer = Completer();

Stream<List<PurchaseDetails>> stream = iapIosPlatform.purchaseStream;
late StreamSubscription subscription;
subscription = stream.listen((purchaseDetailsList) {
details.addAll(purchaseDetailsList);
purchaseDetailsList.forEach((purchaseDetails) {
if (purchaseDetails.status == PurchaseStatus.canceled) {
completer.complete(purchaseDetails.status);
subscription.cancel();
}
});
});
final AppStorePurchaseParam purchaseParam = AppStorePurchaseParam(
productDetails:
AppStoreProductDetails.fromSKProduct(dummyProductWrapper),
applicationUserName: 'appName');
await iapIosPlatform.buyNonConsumable(purchaseParam: purchaseParam);

PurchaseStatus purchaseStatus = await completer.future;
expect(purchaseStatus, PurchaseStatus.canceled);
});
});

group('complete purchase', () {
Expand Down