From d7dc96c5acac9ff16ada8e14466c4a7305e56051 Mon Sep 17 00:00:00 2001 From: yusufdag Date: Thu, 24 Jun 2021 13:03:21 +0200 Subject: [PATCH 01/10] Refactor toPurchaseStatus method to determine cancellation --- .../lib/src/store_kit_wrappers/enum_converters.dart | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/packages/in_app_purchase/in_app_purchase_ios/lib/src/store_kit_wrappers/enum_converters.dart b/packages/in_app_purchase/in_app_purchase_ios/lib/src/store_kit_wrappers/enum_converters.dart index 08af2c6058c4..c375fe89c44c 100644 --- a/packages/in_app_purchase/in_app_purchase_ios/lib/src/store_kit_wrappers/enum_converters.dart +++ b/packages/in_app_purchase/in_app_purchase_ios/lib/src/store_kit_wrappers/enum_converters.dart @@ -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: @@ -40,6 +41,10 @@ class SKTransactionStatusConverter case SKPaymentTransactionStateWrapper.restored: return PurchaseStatus.restored; case SKPaymentTransactionStateWrapper.failed: + if (error != null && (error.code == 2 || error.code == 15)) { + return PurchaseStatus.canceled; + } + return PurchaseStatus.error; case SKPaymentTransactionStateWrapper.unspecified: return PurchaseStatus.error; } From 31a8142b0284c4b34b0d7e8f0f9c6f87e87bac22 Mon Sep 17 00:00:00 2001 From: yusufdag Date: Thu, 24 Jun 2021 13:04:24 +0200 Subject: [PATCH 02/10] Refactor fromSKTransaction to set canceled status --- .../lib/src/types/app_store_purchase_details.dart | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/in_app_purchase/in_app_purchase_ios/lib/src/types/app_store_purchase_details.dart b/packages/in_app_purchase/in_app_purchase_ios/lib/src/types/app_store_purchase_details.dart index 6d6f241d6ca8..58e965a1f19f 100644 --- a/packages/in_app_purchase/in_app_purchase_ios/lib/src/types/app_store_purchase_details.dart +++ b/packages/in_app_purchase/in_app_purchase_ios/lib/src/types/app_store_purchase_details.dart @@ -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, @@ -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, From b7f895f0d47a491e835da2e28ee35bc709f4c277 Mon Sep 17 00:00:00 2001 From: yusufdag Date: Thu, 24 Jun 2021 13:09:28 +0200 Subject: [PATCH 03/10] Fix the comment --- .../in_app_purchase_ios/test/fakes/fake_ios_platform.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/in_app_purchase/in_app_purchase_ios/test/fakes/fake_ios_platform.dart b/packages/in_app_purchase/in_app_purchase_ios/test/fakes/fake_ios_platform.dart index ac5c499768a1..3e5b3c31b543 100644 --- a/packages/in_app_purchase/in_app_purchase_ios/test/fakes/fake_ios_platform.dart +++ b/packages/in_app_purchase/in_app_purchase_ios/test/fakes/fake_ios_platform.dart @@ -18,7 +18,7 @@ class FakeIOSPlatform { channel.setMockMethodCallHandler(onMethodCall); } - // pre-configured store informations + // pre-configured store information String? receiptData; late Set validProductIDs; late Map validProducts; From 9fa4e524d407730ca857a9a7b7fd22485c7b1cbe Mon Sep 17 00:00:00 2001 From: yusufdag Date: Thu, 24 Jun 2021 13:11:55 +0200 Subject: [PATCH 04/10] Add integer to call createCanceledTransaction --- .../in_app_purchase_ios/test/fakes/fake_ios_platform.dart | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/in_app_purchase/in_app_purchase_ios/test/fakes/fake_ios_platform.dart b/packages/in_app_purchase/in_app_purchase_ios/test/fakes/fake_ios_platform.dart index 3e5b3c31b543..5f2bfaedf839 100644 --- a/packages/in_app_purchase/in_app_purchase_ios/test/fakes/fake_ios_platform.dart +++ b/packages/in_app_purchase/in_app_purchase_ios/test/fakes/fake_ios_platform.dart @@ -26,6 +26,7 @@ class FakeIOSPlatform { late List finishedTransactions; late bool testRestoredTransactionsNull; late bool testTransactionFail; + late int testTransactionCancel; PlatformException? queryProductException; PlatformException? restoreException; SKError? testRestoredError; @@ -64,6 +65,7 @@ class FakeIOSPlatform { finishedTransactions = []; testRestoredTransactionsNull = false; testTransactionFail = false; + testTransactionCancel = -1; queryProductException = null; restoreException = null; testRestoredError = null; From f2bf63ccf5255c8a43e846be41fc7687c4504c04 Mon Sep 17 00:00:00 2001 From: yusufdag Date: Thu, 24 Jun 2021 13:15:58 +0200 Subject: [PATCH 05/10] Add createCanceledTransaction method --- .../test/fakes/fake_ios_platform.dart | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/packages/in_app_purchase/in_app_purchase_ios/test/fakes/fake_ios_platform.dart b/packages/in_app_purchase/in_app_purchase_ios/test/fakes/fake_ios_platform.dart index 5f2bfaedf839..660bf11e9746 100644 --- a/packages/in_app_purchase/in_app_purchase_ios/test/fakes/fake_ios_platform.dart +++ b/packages/in_app_purchase/in_app_purchase_ios/test/fakes/fake_ios_platform.dart @@ -106,6 +106,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 onMethodCall(MethodCall call) { switch (call.method) { case '-[SKPaymentQueue canMakePayments:]': @@ -167,6 +181,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( From 6f96739be0babff4d279482b1c1b405113dce157 Mon Sep 17 00:00:00 2001 From: yusufdag Date: Thu, 24 Jun 2021 13:33:25 +0200 Subject: [PATCH 06/10] Add parameter since toPurchaseStatus is changed --- .../test/in_app_purchase_ios_platform_test.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/in_app_purchase/in_app_purchase_ios/test/in_app_purchase_ios_platform_test.dart b/packages/in_app_purchase/in_app_purchase_ios/test/in_app_purchase_ios_platform_test.dart index 973b9d1da0fb..4364828ce382 100644 --- a/packages/in_app_purchase/in_app_purchase_ios/test/in_app_purchase_ios_platform_test.dart +++ b/packages/in_app_purchase/in_app_purchase_ios/test/in_app_purchase_ios_platform_test.dart @@ -128,7 +128,7 @@ void main() { expect( actual.status, SKTransactionStatusConverter() - .toPurchaseStatus(expected.transactionState), + .toPurchaseStatus(expected.transactionState, expected.error), ); expect(actual.verificationData.localVerificationData, fakeIOSPlatform.receiptData); From a383c8a95e885a6580307b694102cf3a6af2b1f2 Mon Sep 17 00:00:00 2001 From: yusufdag Date: Thu, 24 Jun 2021 13:33:50 +0200 Subject: [PATCH 07/10] Add unit test for cancel status --- .../in_app_purchase_ios_platform_test.dart | 54 +++++++++++++++++++ 1 file changed, 54 insertions(+) diff --git a/packages/in_app_purchase/in_app_purchase_ios/test/in_app_purchase_ios_platform_test.dart b/packages/in_app_purchase/in_app_purchase_ios/test/in_app_purchase_ios_platform_test.dart index 4364828ce382..5d6dc7dbe62d 100644 --- a/packages/in_app_purchase/in_app_purchase_ios/test/in_app_purchase_ios_platform_test.dart +++ b/packages/in_app_purchase/in_app_purchase_ios/test/in_app_purchase_ios_platform_test.dart @@ -273,6 +273,60 @@ 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 details = []; + Completer completer = Completer(); + + Stream> 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 details = []; + Completer completer = Completer(); + + Stream> 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', () { From 8dd89d93be794ad3a139e161c4177911d7cae3f8 Mon Sep 17 00:00:00 2001 From: yusufdag Date: Thu, 24 Jun 2021 13:44:39 +0200 Subject: [PATCH 08/10] Update the version and CHANGELOG --- packages/in_app_purchase/in_app_purchase_ios/CHANGELOG.md | 5 +++++ packages/in_app_purchase/in_app_purchase_ios/pubspec.yaml | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/packages/in_app_purchase/in_app_purchase_ios/CHANGELOG.md b/packages/in_app_purchase/in_app_purchase_ios/CHANGELOG.md index 4b2d8ce1dc24..b261f9a3abbd 100644 --- a/packages/in_app_purchase/in_app_purchase_ios/CHANGELOG.md +++ b/packages/in_app_purchase/in_app_purchase_ios/CHANGELOG.md @@ -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.0+2 * Changed the iOS payment queue handler in such a way that it only adds a listener to the SKPaymentQueue when there diff --git a/packages/in_app_purchase/in_app_purchase_ios/pubspec.yaml b/packages/in_app_purchase/in_app_purchase_ios/pubspec.yaml index 5b9e3892d40d..5f65ac01e4d8 100644 --- a/packages/in_app_purchase/in_app_purchase_ios/pubspec.yaml +++ b/packages/in_app_purchase/in_app_purchase_ios/pubspec.yaml @@ -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.0+2 +version: 0.2.0 environment: sdk: ">=2.12.0 <3.0.0" From a806b48cc5435eb25fbc0ff0443c5fd6d6e63737 Mon Sep 17 00:00:00 2001 From: yusufdag Date: Wed, 7 Jul 2021 12:25:19 +0200 Subject: [PATCH 09/10] Add error code explanation comment --- .../lib/src/store_kit_wrappers/enum_converters.dart | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/in_app_purchase/in_app_purchase_ios/lib/src/store_kit_wrappers/enum_converters.dart b/packages/in_app_purchase/in_app_purchase_ios/lib/src/store_kit_wrappers/enum_converters.dart index c375fe89c44c..3e686825ea6e 100644 --- a/packages/in_app_purchase/in_app_purchase_ios/lib/src/store_kit_wrappers/enum_converters.dart +++ b/packages/in_app_purchase/in_app_purchase_ios/lib/src/store_kit_wrappers/enum_converters.dart @@ -41,6 +41,10 @@ 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; } From dc1a1fd9786dc0d7ce3a439d16ab14d1e625ffff Mon Sep 17 00:00:00 2001 From: yusufdag Date: Wed, 7 Jul 2021 12:28:02 +0200 Subject: [PATCH 10/10] Add empty line to separate the tests --- .../test/in_app_purchase_ios_platform_test.dart | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/in_app_purchase/in_app_purchase_ios/test/in_app_purchase_ios_platform_test.dart b/packages/in_app_purchase/in_app_purchase_ios/test/in_app_purchase_ios_platform_test.dart index 5d6dc7dbe62d..7603426abc68 100644 --- a/packages/in_app_purchase/in_app_purchase_ios/test/in_app_purchase_ios_platform_test.dart +++ b/packages/in_app_purchase/in_app_purchase_ios/test/in_app_purchase_ios_platform_test.dart @@ -273,6 +273,7 @@ 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 { @@ -300,6 +301,7 @@ void main() { PurchaseStatus purchaseStatus = await completer.future; expect(purchaseStatus, PurchaseStatus.canceled); }); + test( 'should get canceled purchase status when error code is SKErrorOverlayCancelled', () async {