[in_app_purchase] Emit empty list when no transactions to restore. (#4523)
diff --git a/packages/in_app_purchase/in_app_purchase_storekit/CHANGELOG.md b/packages/in_app_purchase/in_app_purchase_storekit/CHANGELOG.md
index ff5bffd..e565812 100644
--- a/packages/in_app_purchase/in_app_purchase_storekit/CHANGELOG.md
+++ b/packages/in_app_purchase/in_app_purchase_storekit/CHANGELOG.md
@@ -1,3 +1,7 @@
+## 0.3.0
+
+* **BREAKING CHANGE:** `InAppPurchaseStoreKitPlatform.restorePurchase()` emits an empty instance of `List<ProductDetails>` when there were no transactions to restore, indicating that the restore procedure has finished.
+
## 0.2.1
* Renames `in_app_purchase_ios` to `in_app_purchase_storekit` to facilitate
diff --git a/packages/in_app_purchase/in_app_purchase_storekit/lib/src/in_app_purchase_storekit_platform.dart b/packages/in_app_purchase/in_app_purchase_storekit/lib/src/in_app_purchase_storekit_platform.dart
index 65b1a30..e912dd6 100644
--- a/packages/in_app_purchase/in_app_purchase_storekit/lib/src/in_app_purchase_storekit_platform.dart
+++ b/packages/in_app_purchase/in_app_purchase_storekit/lib/src/in_app_purchase_storekit_platform.dart
@@ -153,11 +153,19 @@
}
}
+enum _TransactionRestoreState {
+ notRunning,
+ waitingForTransactions,
+ receivedTransaction,
+}
+
class _TransactionObserver implements SKTransactionObserverWrapper {
final StreamController<List<PurchaseDetails>> purchaseUpdatedController;
Completer? _restoreCompleter;
late String _receiptData;
+ _TransactionRestoreState _transactionRestoreState =
+ _TransactionRestoreState.notRunning;
_TransactionObserver(this.purchaseUpdatedController);
@@ -165,6 +173,7 @@
required SKPaymentQueueWrapper queue,
String? applicationUserName,
}) {
+ _transactionRestoreState = _TransactionRestoreState.waitingForTransactions;
_restoreCompleter = Completer();
queue.restoreTransactions(applicationUserName: applicationUserName);
return _restoreCompleter!.future;
@@ -176,6 +185,14 @@
void updatedTransactions(
{required List<SKPaymentTransactionWrapper> transactions}) async {
+ if (_transactionRestoreState ==
+ _TransactionRestoreState.waitingForTransactions &&
+ transactions.any((transaction) =>
+ transaction.transactionState ==
+ SKPaymentTransactionStateWrapper.restored)) {
+ _transactionRestoreState = _TransactionRestoreState.receivedTransaction;
+ }
+
String receiptData = await getReceiptData();
List<PurchaseDetails> purchases = transactions
.map((SKPaymentTransactionWrapper transaction) =>
@@ -191,10 +208,21 @@
/// Triggered when there is an error while restoring transactions.
void restoreCompletedTransactionsFailed({required SKError error}) {
_restoreCompleter!.completeError(error);
+ _transactionRestoreState = _TransactionRestoreState.notRunning;
}
void paymentQueueRestoreCompletedTransactionsFinished() {
_restoreCompleter!.complete();
+
+ // If no restored transactions were received during the restore session
+ // emit an empty list of purchase details to inform listeners that the
+ // restore session finished without any results.
+ if (_transactionRestoreState ==
+ _TransactionRestoreState.waitingForTransactions) {
+ purchaseUpdatedController.add(<PurchaseDetails>[]);
+ }
+
+ _transactionRestoreState = _TransactionRestoreState.notRunning;
}
bool shouldAddStorePayment(
diff --git a/packages/in_app_purchase/in_app_purchase_storekit/pubspec.yaml b/packages/in_app_purchase/in_app_purchase_storekit/pubspec.yaml
index bfc55b9..1fe291d 100644
--- a/packages/in_app_purchase/in_app_purchase_storekit/pubspec.yaml
+++ b/packages/in_app_purchase/in_app_purchase_storekit/pubspec.yaml
@@ -2,7 +2,7 @@
description: An implementation for the iOS platform of the Flutter `in_app_purchase` plugin. This uses the StoreKit Framework.
repository: https://github.com/flutter/plugins/tree/master/packages/in_app_purchase/in_app_purchase_storekit
issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+in_app_purchase%22
-version: 0.2.1
+version: 0.3.0
environment:
sdk: ">=2.14.0 <3.0.0"
diff --git a/packages/in_app_purchase/in_app_purchase_storekit/test/fakes/fake_storekit_platform.dart b/packages/in_app_purchase/in_app_purchase_storekit/test/fakes/fake_storekit_platform.dart
index c4254c2..352ba32 100644
--- a/packages/in_app_purchase/in_app_purchase_storekit/test/fakes/fake_storekit_platform.dart
+++ b/packages/in_app_purchase/in_app_purchase_storekit/test/fakes/fake_storekit_platform.dart
@@ -47,24 +47,6 @@
validProducts[validID] = SKProductWrapper.fromJson(productWrapperMap);
}
- SKPaymentTransactionWrapper tran1 = SKPaymentTransactionWrapper(
- transactionIdentifier: '123',
- payment: dummyPayment,
- originalTransaction: dummyTransaction,
- transactionTimeStamp: 123123123.022,
- transactionState: SKPaymentTransactionStateWrapper.restored,
- error: null,
- );
- SKPaymentTransactionWrapper tran2 = SKPaymentTransactionWrapper(
- transactionIdentifier: '1234',
- payment: dummyPayment,
- originalTransaction: dummyTransaction,
- transactionTimeStamp: 123123123.022,
- transactionState: SKPaymentTransactionStateWrapper.restored,
- error: null,
- );
-
- transactions.addAll([tran1, tran2]);
finishedTransactions = [];
testRestoredTransactionsNull = false;
testTransactionFail = false;
@@ -123,6 +105,17 @@
originalTransaction: null);
}
+ SKPaymentTransactionWrapper createRestoredTransaction(
+ String productId, String transactionId) {
+ return SKPaymentTransactionWrapper(
+ payment: SKPaymentWrapper(productIdentifier: productId),
+ transactionState: SKPaymentTransactionStateWrapper.restored,
+ transactionTimeStamp: 123123.121,
+ transactionIdentifier: transactionId,
+ error: null,
+ originalTransaction: null);
+ }
+
Future<dynamic> onMethodCall(MethodCall call) {
switch (call.method) {
case '-[SKPaymentQueue canMakePayments:]':
diff --git a/packages/in_app_purchase/in_app_purchase_storekit/test/in_app_purchase_storekit_platform_test.dart b/packages/in_app_purchase/in_app_purchase_storekit/test/in_app_purchase_storekit_platform_test.dart
index 8ac4b04..12595bb 100644
--- a/packages/in_app_purchase/in_app_purchase_storekit/test/in_app_purchase_storekit_platform_test.dart
+++ b/packages/in_app_purchase/in_app_purchase_storekit/test/in_app_purchase_storekit_platform_test.dart
@@ -78,14 +78,18 @@
group('restore purchases', () {
test('should emit restored transactions on purchase stream', () async {
+ fakeStoreKitPlatform.transactions.insert(
+ 0, fakeStoreKitPlatform.createRestoredTransaction('foo', 'RT1'));
+ fakeStoreKitPlatform.transactions.insert(
+ 1, fakeStoreKitPlatform.createRestoredTransaction('foo', 'RT2'));
Completer completer = Completer();
Stream<List<PurchaseDetails>> stream = iapStoreKitPlatform.purchaseStream;
late StreamSubscription subscription;
subscription = stream.listen((purchaseDetailsList) {
if (purchaseDetailsList.first.status == PurchaseStatus.restored) {
- completer.complete(purchaseDetailsList);
subscription.cancel();
+ completer.complete(purchaseDetailsList);
}
});
@@ -109,15 +113,37 @@
}
});
- test('should not block transaction updates', () async {
- fakeStoreKitPlatform.transactions.insert(
- 0, fakeStoreKitPlatform.createPurchasedTransaction('foo', 'bar'));
+ test(
+ 'should emit empty transaction list on purchase stream when there is nothing to restore',
+ () async {
+ fakeStoreKitPlatform.testRestoredTransactionsNull = true;
Completer completer = Completer();
Stream<List<PurchaseDetails>> stream = iapStoreKitPlatform.purchaseStream;
late StreamSubscription subscription;
subscription = stream.listen((purchaseDetailsList) {
- if (purchaseDetailsList.first.status == PurchaseStatus.purchased) {
+ expect(purchaseDetailsList.isEmpty, true);
+ subscription.cancel();
+ completer.complete();
+ });
+
+ await iapStoreKitPlatform.restorePurchases();
+ await completer.future;
+ });
+
+ test('should not block transaction updates', () async {
+ fakeStoreKitPlatform.transactions.insert(
+ 0, fakeStoreKitPlatform.createRestoredTransaction('foo', 'RT1'));
+ fakeStoreKitPlatform.transactions.insert(
+ 1, fakeStoreKitPlatform.createPurchasedTransaction('foo', 'bar'));
+ fakeStoreKitPlatform.transactions.insert(
+ 2, fakeStoreKitPlatform.createRestoredTransaction('foo', 'RT2'));
+ Completer completer = Completer();
+ Stream<List<PurchaseDetails>> stream = iapStoreKitPlatform.purchaseStream;
+
+ late StreamSubscription subscription;
+ subscription = stream.listen((purchaseDetailsList) {
+ if (purchaseDetailsList[1].status == PurchaseStatus.purchased) {
completer.complete(purchaseDetailsList);
subscription.cancel();
}
@@ -145,8 +171,54 @@
}
});
+ test(
+ 'should emit empty transaction if transactions array does not contain a transaction with PurchaseStatus.restored status.',
+ () async {
+ fakeStoreKitPlatform.transactions.insert(
+ 0, fakeStoreKitPlatform.createPurchasedTransaction('foo', 'bar'));
+ Completer completer = Completer();
+ Stream<List<PurchaseDetails>> stream = iapStoreKitPlatform.purchaseStream;
+ List<List<PurchaseDetails>> purchaseDetails = [];
+
+ late StreamSubscription subscription;
+ subscription = stream.listen((purchaseDetailsList) {
+ purchaseDetails.add(purchaseDetailsList);
+
+ if (purchaseDetails.length == 2) {
+ completer.complete(purchaseDetails);
+ subscription.cancel();
+ }
+ });
+ await iapStoreKitPlatform.restorePurchases();
+ final details = await completer.future;
+ expect(details.length, 2);
+ expect(details[0], []);
+ for (int i = 0; i < fakeStoreKitPlatform.transactions.length; i++) {
+ SKPaymentTransactionWrapper expected =
+ fakeStoreKitPlatform.transactions[i];
+ PurchaseDetails actual = details[1][i];
+
+ expect(actual.purchaseID, expected.transactionIdentifier);
+ expect(actual.verificationData, isNotNull);
+ expect(
+ actual.status,
+ SKTransactionStatusConverter()
+ .toPurchaseStatus(expected.transactionState, expected.error),
+ );
+ expect(actual.verificationData.localVerificationData,
+ fakeStoreKitPlatform.receiptData);
+ expect(actual.verificationData.serverVerificationData,
+ fakeStoreKitPlatform.receiptData);
+ expect(actual.pendingCompletePurchase, true);
+ }
+ });
+
test('receipt error should populate null to verificationData.data',
() async {
+ fakeStoreKitPlatform.transactions.insert(
+ 0, fakeStoreKitPlatform.createRestoredTransaction('foo', 'RT1'));
+ fakeStoreKitPlatform.transactions.insert(
+ 1, fakeStoreKitPlatform.createRestoredTransaction('foo', 'RT2'));
fakeStoreKitPlatform.receiptData = null;
Completer completer = Completer();
Stream<List<PurchaseDetails>> stream = iapStoreKitPlatform.purchaseStream;