Skip to content
This repository was archived by the owner on Feb 22, 2023. It is now read-only.
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.0+2

* Changed the iOS payment queue handler in such a way that it only adds a listener to the SKPaymentQueue when there
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,10 @@ class SKTransactionStatusConverter
case SKPaymentTransactionStateWrapper.restored:
return PurchaseStatus.restored;
case SKPaymentTransactionStateWrapper.failed:
if (error != null && (error.code == 2 || error.code == 15)) {
Comment thread
ydag marked this conversation as resolved.
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
2 changes: 1 addition & 1 deletion 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.0+2
version: 0.2.0

environment:
sdk: ">=2.12.0 <3.0.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 @@ -64,6 +65,7 @@ class FakeIOSPlatform {
finishedTransactions = [];
testRestoredTransactionsNull = false;
testTransactionFail = false;
testTransactionCancel = -1;
queryProductException = null;
restoreException = null;
testRestoredError = null;
Expand Down Expand Up @@ -104,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<dynamic> onMethodCall(MethodCall call) {
switch (call.method) {
case '-[SKPaymentQueue canMakePayments:]':
Expand Down Expand Up @@ -165,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(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,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 @@ -273,6 +273,60 @@ void main() {
expect(completerError.message, 'ios_domain');
expect(completerError.details, {'message': 'an error message'});
});
test(
Comment thread
ydag marked this conversation as resolved.
'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(
Comment thread
ydag marked this conversation as resolved.
'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