Skip to content

Commit 05b1010

Browse files
authored
[in_app_purchase] Fix finishing purchases upon payment cancellation (flutter#3106)
1 parent 2077363 commit 05b1010

File tree

7 files changed

+43
-24
lines changed

7 files changed

+43
-24
lines changed

packages/in_app_purchase/CHANGELOG.md

+3
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
## 0.3.4+12
2+
3+
* [iOS] Fixed: finishing purchases upon payment dialog cancellation.
14

25
## 0.3.4+11
36

packages/in_app_purchase/ios/Classes/InAppPurchasePlugin.m

+12-4
Original file line numberDiff line numberDiff line change
@@ -199,19 +199,27 @@ - (void)addPayment:(FlutterMethodCall *)call result:(FlutterResult)result {
199199
}
200200

201201
- (void)finishTransaction:(FlutterMethodCall *)call result:(FlutterResult)result {
202-
if (![call.arguments isKindOfClass:[NSString class]]) {
202+
if (![call.arguments isKindOfClass:[NSDictionary class]]) {
203203
result([FlutterError errorWithCode:@"storekit_invalid_argument"
204-
message:@"Argument type of finishTransaction is not a string."
204+
message:@"Argument type of finishTransaction is not a Dictionary"
205205
details:call.arguments]);
206206
return;
207207
}
208-
NSString *transactionIdentifier = call.arguments;
208+
NSDictionary *paymentMap = (NSDictionary *)call.arguments;
209+
NSString *transactionIdentifier = [paymentMap objectForKey:@"transactionIdentifier"];
210+
NSString *productIdentifier = [paymentMap objectForKey:@"productIdentifier"];
209211

210212
NSArray<SKPaymentTransaction *> *pendingTransactions =
211213
[self.paymentQueueHandler getUnfinishedTransactions];
212214

213215
for (SKPaymentTransaction *transaction in pendingTransactions) {
214-
if ([transaction.transactionIdentifier isEqualToString:transactionIdentifier]) {
216+
// If the user cancels the purchase dialog we won't have a transactionIdentifier.
217+
// So if it is null AND a transaction in the pendingTransactions list has
218+
// also a null transactionIdentifier we check for equal product identifiers.
219+
if ([transaction.transactionIdentifier isEqualToString:transactionIdentifier] ||
220+
([transactionIdentifier isEqual:[NSNull null]] &&
221+
transaction.transactionIdentifier == nil &&
222+
[transaction.payment.productIdentifier isEqualToString:productIdentifier])) {
215223
@try {
216224
[self.paymentQueueHandler finishTransaction:transaction];
217225
} @catch (NSException *e) {

packages/in_app_purchase/lib/src/store_kit_wrappers/sk_payment_queue_wrapper.dart

+4-2
Original file line numberDiff line numberDiff line change
@@ -103,9 +103,11 @@ class SKPaymentQueueWrapper {
103103
/// finishTransaction:]`](https://developer.apple.com/documentation/storekit/skpaymentqueue/1506003-finishtransaction?language=objc).
104104
Future<void> finishTransaction(
105105
SKPaymentTransactionWrapper transaction) async {
106+
Map<String, String> requestMap = transaction.toFinishMap();
106107
await channel.invokeMethod<void>(
107-
'-[InAppPurchasePlugin finishTransaction:result:]',
108-
transaction.transactionIdentifier);
108+
'-[InAppPurchasePlugin finishTransaction:result:]',
109+
requestMap,
110+
);
109111
}
110112

111113
/// Restore previously purchased transactions.

packages/in_app_purchase/lib/src/store_kit_wrappers/sk_payment_transaction_wrappers.dart

+6
Original file line numberDiff line numberDiff line change
@@ -185,4 +185,10 @@ class SKPaymentTransactionWrapper {
185185

186186
@override
187187
String toString() => _$SKPaymentTransactionWrapperToJson(this).toString();
188+
189+
/// The payload that is used to finish this transaction.
190+
Map<String, String> toFinishMap() => {
191+
"transactionIdentifier": this.transactionIdentifier,
192+
"productIdentifier": this.payment?.productIdentifier,
193+
};
188194
}

packages/in_app_purchase/pubspec.yaml

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
name: in_app_purchase
22
description: A Flutter plugin for in-app purchases. Exposes APIs for making in-app purchases through the App Store and Google Play.
33
homepage: https://github.com/flutter/plugins/tree/master/packages/in_app_purchase
4-
version: 0.3.4+11
4+
version: 0.3.4+12
55

66
dependencies:
77
async: ^2.0.8

packages/in_app_purchase/test/in_app_purchase_connection/app_store_connection_test.dart

+14-14
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,7 @@ void main() {
9292

9393
test('queryPastPurchases should not block transaction updates', () async {
9494
fakeIOSPlatform.transactions
95-
.add(fakeIOSPlatform.createPurchasedTransactionWithProductID('foo'));
95+
.add(fakeIOSPlatform.createPurchasedTransaction('foo', 'bar'));
9696
Completer completer = Completer();
9797
Stream<List<PurchaseDetails>> stream =
9898
AppStoreConnection.instance.purchaseUpdatedStream;
@@ -348,7 +348,7 @@ class FakeIOSPlatform {
348348
testRestoredError = null;
349349
}
350350

351-
SKPaymentTransactionWrapper createPendingTransactionWithProductID(String id) {
351+
SKPaymentTransactionWrapper createPendingTransaction(String id) {
352352
return SKPaymentTransactionWrapper(
353353
transactionIdentifier: null,
354354
payment: SKPaymentWrapper(productIdentifier: id),
@@ -358,21 +358,21 @@ class FakeIOSPlatform {
358358
originalTransaction: null);
359359
}
360360

361-
SKPaymentTransactionWrapper createPurchasedTransactionWithProductID(
362-
String id) {
361+
SKPaymentTransactionWrapper createPurchasedTransaction(
362+
String productId, String transactionId) {
363363
return SKPaymentTransactionWrapper(
364-
payment: SKPaymentWrapper(productIdentifier: id),
364+
payment: SKPaymentWrapper(productIdentifier: productId),
365365
transactionState: SKPaymentTransactionStateWrapper.purchased,
366366
transactionTimeStamp: 123123.121,
367-
transactionIdentifier: id,
367+
transactionIdentifier: transactionId,
368368
error: null,
369369
originalTransaction: null);
370370
}
371371

372-
SKPaymentTransactionWrapper createFailedTransactionWithProductID(String id) {
372+
SKPaymentTransactionWrapper createFailedTransaction(String productId) {
373373
return SKPaymentTransactionWrapper(
374374
transactionIdentifier: null,
375-
payment: SKPaymentWrapper(productIdentifier: id),
375+
payment: SKPaymentWrapper(productIdentifier: productId),
376376
transactionState: SKPaymentTransactionStateWrapper.failed,
377377
transactionTimeStamp: 123123.121,
378378
error: SKError(
@@ -434,26 +434,26 @@ class FakeIOSPlatform {
434434
return Future<void>.sync(() {});
435435
case '-[InAppPurchasePlugin addPayment:result:]':
436436
String id = call.arguments['productIdentifier'];
437-
SKPaymentTransactionWrapper transaction =
438-
createPendingTransactionWithProductID(id);
437+
SKPaymentTransactionWrapper transaction = createPendingTransaction(id);
439438
AppStoreConnection.observer
440439
.updatedTransactions(transactions: [transaction]);
441440
sleep(const Duration(milliseconds: 30));
442441
if (testTransactionFail) {
443442
SKPaymentTransactionWrapper transaction_failed =
444-
createFailedTransactionWithProductID(id);
443+
createFailedTransaction(id);
445444
AppStoreConnection.observer
446445
.updatedTransactions(transactions: [transaction_failed]);
447446
} else {
448447
SKPaymentTransactionWrapper transaction_finished =
449-
createPurchasedTransactionWithProductID(id);
448+
createPurchasedTransaction(id, transaction.transactionIdentifier);
450449
AppStoreConnection.observer
451450
.updatedTransactions(transactions: [transaction_finished]);
452451
}
453452
break;
454453
case '-[InAppPurchasePlugin finishTransaction:result:]':
455-
finishedTransactions
456-
.add(createPurchasedTransactionWithProductID(call.arguments));
454+
finishedTransactions.add(createPurchasedTransaction(
455+
call.arguments["productIdentifier"],
456+
call.arguments["transactionIdentifier"]));
457457
break;
458458
}
459459
return Future<void>.sync(() {});

packages/in_app_purchase/test/store_kit_wrappers/sk_methodchannel_apis_test.dart

+3-3
Original file line numberDiff line numberDiff line change
@@ -110,7 +110,7 @@ void main() {
110110
queue.setTransactionObserver(observer);
111111
await queue.finishTransaction(dummyTransaction);
112112
expect(fakeIOSPlatform.transactionsFinished.first,
113-
equals(dummyTransaction.transactionIdentifier));
113+
equals(dummyTransaction.toFinishMap()));
114114
});
115115

116116
test('should restore transaction', () async {
@@ -139,7 +139,7 @@ class FakeIOSPlatform {
139139

140140
// payment queue
141141
List<SKPaymentWrapper> payments = [];
142-
List<String> transactionsFinished = [];
142+
List<Map<String, String>> transactionsFinished = [];
143143
String applicationNameHasTransactionRestored;
144144

145145
Future<dynamic> onMethodCall(MethodCall call) {
@@ -171,7 +171,7 @@ class FakeIOSPlatform {
171171
payments.add(SKPaymentWrapper.fromJson(call.arguments));
172172
return Future<void>.sync(() {});
173173
case '-[InAppPurchasePlugin finishTransaction:result:]':
174-
transactionsFinished.add(call.arguments);
174+
transactionsFinished.add(Map<String, String>.from(call.arguments));
175175
return Future<void>.sync(() {});
176176
case '-[InAppPurchasePlugin restoreTransactions:result:]':
177177
applicationNameHasTransactionRestored = call.arguments;

0 commit comments

Comments
 (0)