diff --git a/packages/in_app_purchase/CHANGELOG.md b/packages/in_app_purchase/CHANGELOG.md index ade5dbe57bc1..fe601c2d5728 100644 --- a/packages/in_app_purchase/CHANGELOG.md +++ b/packages/in_app_purchase/CHANGELOG.md @@ -1,3 +1,9 @@ +## 0.3.4+8 + +* [iOS] Fixed: purchase dialog not showing always. +* [iOS] Fixed: completing purchases could fail. +* [iOS] Fixed: restorePurchases caused hang (call never returned). + ## 0.3.4+7 * iOS: Fix typo of the `simulatesAskToBuyInSandbox` key. diff --git a/packages/in_app_purchase/ios/Classes/FIAPaymentQueueHandler.h b/packages/in_app_purchase/ios/Classes/FIAPaymentQueueHandler.h index 6f0c64bb85df..54898d170304 100644 --- a/packages/in_app_purchase/ios/Classes/FIAPaymentQueueHandler.h +++ b/packages/in_app_purchase/ios/Classes/FIAPaymentQueueHandler.h @@ -18,10 +18,6 @@ typedef void (^UpdatedDownloads)(NSArray *downloads); @interface FIAPaymentQueueHandler : NSObject -// Unfinished transactions. -@property(nonatomic, readonly) - NSDictionary *> *transactions; - - (instancetype)initWithQueue:(nonnull SKPaymentQueue *)queue transactionsUpdated:(nullable TransactionsUpdated)transactionsUpdated transactionRemoved:(nullable TransactionsRemoved)transactionsRemoved diff --git a/packages/in_app_purchase/ios/Classes/FIAPaymentQueueHandler.m b/packages/in_app_purchase/ios/Classes/FIAPaymentQueueHandler.m index ddf9b2736a77..ecbd237c90ce 100644 --- a/packages/in_app_purchase/ios/Classes/FIAPaymentQueueHandler.m +++ b/packages/in_app_purchase/ios/Classes/FIAPaymentQueueHandler.m @@ -15,9 +15,6 @@ @interface FIAPaymentQueueHandler () @property(nullable, copy, nonatomic) ShouldAddStorePayment shouldAddStorePayment; @property(nullable, copy, nonatomic) UpdatedDownloads updatedDownloads; -@property(strong, nonatomic) - NSMutableDictionary *> *transactionsSetter; - @end @implementation FIAPaymentQueueHandler @@ -39,7 +36,6 @@ - (instancetype)initWithQueue:(nonnull SKPaymentQueue *)queue _paymentQueueRestoreCompletedTransactionsFinished = restoreCompletedTransactionsFinished; _shouldAddStorePayment = shouldAddStorePayment; _updatedDownloads = updatedDownloads; - _transactionsSetter = [NSMutableDictionary dictionary]; } return self; } @@ -49,8 +45,10 @@ - (void)startObservingPaymentQueue { } - (BOOL)addPayment:(SKPayment *)payment { - if (self.transactionsSetter[payment.productIdentifier]) { - return NO; + for (SKPaymentTransaction *transaction in self.queue.transactions) { + if ([transaction.payment.productIdentifier isEqualToString:payment.productIdentifier]) { + return NO; + } } [self.queue addPayment:payment]; return YES; @@ -74,22 +72,6 @@ - (void)restoreTransactions:(nullable NSString *)applicationName { // state of transactions and finish as appropriate. - (void)paymentQueue:(SKPaymentQueue *)queue updatedTransactions:(NSArray *)transactions { - for (SKPaymentTransaction *transaction in transactions) { - if (transaction.transactionState != SKPaymentTransactionStatePurchasing) { - // Use product identifier instead of transaction identifier for few reasons: - // 1. Only transactions with purchased state and failed state will have a transaction id, it - // will become impossible for clients to finish deferred transactions when needed. - // 2. Using product identifiers can help prevent clients from purchasing the same - // subscription more than once by accident. - NSMutableArray *transactionArray = - [self.transactionsSetter objectForKey:transaction.payment.productIdentifier]; - if (transactionArray == nil) { - transactionArray = [NSMutableArray array]; - } - [transactionArray addObject:transaction]; - self.transactionsSetter[transaction.payment.productIdentifier] = transactionArray; - } - } // notify dart through callbacks. self.transactionsUpdated(transactions); } @@ -97,23 +79,6 @@ - (void)paymentQueue:(SKPaymentQueue *)queue // Sent when transactions are removed from the queue (via finishTransaction:). - (void)paymentQueue:(SKPaymentQueue *)queue removedTransactions:(NSArray *)transactions { - for (SKPaymentTransaction *transaction in transactions) { - NSString *productId = transaction.payment.productIdentifier; - - if ([self.transactionsSetter objectForKey:productId] == nil) { - continue; - } - - NSPredicate *predicate = [NSPredicate - predicateWithFormat:@"transactionIdentifier == %@", transaction.transactionIdentifier]; - NSArray *filteredTransactions = - [self.transactionsSetter[productId] filteredArrayUsingPredicate:predicate]; - [self.transactionsSetter[productId] removeObjectsInArray:filteredTransactions]; - - if (!self.transactionsSetter[productId] || !self.transactionsSetter[productId].count) { - [self.transactionsSetter removeObjectForKey:productId]; - } - } self.transactionsRemoved(transactions); } @@ -146,10 +111,4 @@ - (BOOL)paymentQueue:(SKPaymentQueue *)queue return self.queue.transactions; } -#pragma mark - getter - -- (NSDictionary *> *)transactions { - return [self.transactionsSetter copy]; -} - @end diff --git a/packages/in_app_purchase/ios/Classes/InAppPurchasePlugin.m b/packages/in_app_purchase/ios/Classes/InAppPurchasePlugin.m index dcf27147eb1d..156ce0c33e8f 100644 --- a/packages/in_app_purchase/ios/Classes/InAppPurchasePlugin.m +++ b/packages/in_app_purchase/ios/Classes/InAppPurchasePlugin.m @@ -203,32 +203,24 @@ - (void)finishTransaction:(FlutterMethodCall *)call result:(FlutterResult)result details:call.arguments]); return; } - NSString *identifier = call.arguments; - NSMutableArray *transactions = [self.paymentQueueHandler.transactions objectForKey:identifier]; - if (!transactions) { - result([FlutterError - errorWithCode:@"storekit_platform_invalid_transaction" - message:[NSString - stringWithFormat:@"The transaction with transactionIdentifer:%@ does not " - @"exist. Note that if the transactionState is " - @"purchasing, the transactionIdentifier will be " - @"nil(null).", - identifier] - details:call.arguments]); - return; - } - @try { - for (SKPaymentTransaction *transaction in transactions) { - [self.paymentQueueHandler finishTransaction:transaction]; + NSString *transactionIdentifier = call.arguments; + + NSArray *pendingTransactions = + [self.paymentQueueHandler getUnfinishedTransactions]; + + for (SKPaymentTransaction *transaction in pendingTransactions) { + if ([transaction.transactionIdentifier isEqualToString:transactionIdentifier]) { + @try { + [self.paymentQueueHandler finishTransaction:transaction]; + } @catch (NSException *e) { + result([FlutterError errorWithCode:@"storekit_finish_transaction_exception" + message:e.name + details:e.description]); + return; + } } - // finish transaction will throw exception if the transaction type is purchasing. Notify dart - // about this exception. - } @catch (NSException *e) { - result([FlutterError errorWithCode:@"storekit_finish_transaction_exception" - message:e.name - details:e.description]); - return; } + result(nil); } @@ -241,6 +233,7 @@ - (void)restoreTransactions:(FlutterMethodCall *)call result:(FlutterResult)resu return; } [self.paymentQueueHandler restoreTransactions:call.arguments]; + result(nil); } - (void)retrieveReceiptData:(FlutterMethodCall *)call result:(FlutterResult)result { diff --git a/packages/in_app_purchase/ios/Tests/InAppPurchasePluginTest.m b/packages/in_app_purchase/ios/Tests/InAppPurchasePluginTest.m index 9c654b0e9c4c..369e0ead0ba7 100644 --- a/packages/in_app_purchase/ios/Tests/InAppPurchasePluginTest.m +++ b/packages/in_app_purchase/ios/Tests/InAppPurchasePluginTest.m @@ -110,51 +110,6 @@ - (void)testAddPaymentFailure { XCTAssertEqual(transactionForUpdateBlock.transactionState, SKPaymentTransactionStateFailed); } -- (void)testAddPaymentWithSameProductIDWillFail { - XCTestExpectation* expectation = - [self expectationWithDescription:@"result should return expected error"]; - FlutterMethodCall* call = - [FlutterMethodCall methodCallWithMethodName:@"-[InAppPurchasePlugin addPayment:result:]" - arguments:@{ - @"productIdentifier" : @"123", - @"quantity" : @(1), - @"simulatesAskToBuyInSandbox" : @YES, - }]; - SKPaymentQueueStub* queue = [SKPaymentQueueStub new]; - queue.testState = SKPaymentTransactionStatePurchased; - self.plugin.paymentQueueHandler = [[FIAPaymentQueueHandler alloc] initWithQueue:queue - transactionsUpdated:^(NSArray* _Nonnull transactions) { - } - transactionRemoved:nil - restoreTransactionFailed:nil - restoreCompletedTransactionsFinished:nil - shouldAddStorePayment:^BOOL(SKPayment* _Nonnull payment, SKProduct* _Nonnull product) { - return YES; - } - updatedDownloads:nil]; - [queue addTransactionObserver:self.plugin.paymentQueueHandler]; - - FlutterResult addDuplicatePaymentBlock = ^(id r) { - XCTAssertNil(r); - [self.plugin - handleMethodCall:call - result:^(id result) { - XCTAssertNotNil(result); - XCTAssertTrue([result isKindOfClass:[FlutterError class]]); - FlutterError* error = (FlutterError*)result; - XCTAssertEqualObjects(error.code, @"storekit_duplicate_product_object"); - XCTAssertEqualObjects( - error.message, - @"There is a pending transaction for the same product identifier. Please " - @"either wait for it to be finished or finish it manually using " - @"`completePurchase` to avoid edge cases."); - [expectation fulfill]; - }]; - }; - [self.plugin handleMethodCall:call result:addDuplicatePaymentBlock]; - [self waitForExpectations:@[ expectation ] timeout:5]; -} - - (void)testAddPaymentSuccessWithMockQueue { XCTestExpectation* expectation = [self expectationWithDescription:@"result should return success state"]; diff --git a/packages/in_app_purchase/ios/Tests/PaymentQueueTest.m b/packages/in_app_purchase/ios/Tests/PaymentQueueTest.m index 8f5b66496f69..07b6bbb42a65 100644 --- a/packages/in_app_purchase/ios/Tests/PaymentQueueTest.m +++ b/packages/in_app_purchase/ios/Tests/PaymentQueueTest.m @@ -69,26 +69,6 @@ - (void)testTransactionPurchased { XCTAssertEqual(tran.transactionIdentifier, @"fakeID"); } -- (void)testDuplicateTransactionsWillTriggerAnError { - SKPaymentQueueStub *queue = [[SKPaymentQueueStub alloc] init]; - queue.testState = SKPaymentTransactionStatePurchased; - FIAPaymentQueueHandler *handler = [[FIAPaymentQueueHandler alloc] initWithQueue:queue - transactionsUpdated:^(NSArray *_Nonnull transactions) { - } - transactionRemoved:nil - restoreTransactionFailed:nil - restoreCompletedTransactionsFinished:nil - shouldAddStorePayment:^BOOL(SKPayment *_Nonnull payment, SKProduct *_Nonnull product) { - return YES; - } - updatedDownloads:nil]; - [queue addTransactionObserver:handler]; - SKPayment *payment = - [SKPayment paymentWithProduct:[[SKProductStub alloc] initWithMap:self.productResponseMap]]; - XCTAssertTrue([handler addPayment:payment]); - XCTAssertFalse([handler addPayment:payment]); -} - - (void)testTransactionFailed { XCTestExpectation *expectation = [self expectationWithDescription:@"expect to get failed transcation."]; @@ -208,13 +188,11 @@ - (void)testFinishTransaction { queue.testState = SKPaymentTransactionStateDeferred; __block FIAPaymentQueueHandler *handler = [[FIAPaymentQueueHandler alloc] initWithQueue:queue transactionsUpdated:^(NSArray *_Nonnull transactions) { - XCTAssertEqual(handler.transactions.count, 1); XCTAssertEqual(transactions.count, 1); SKPaymentTransaction *transaction = transactions[0]; [handler finishTransaction:transaction]; } transactionRemoved:^(NSArray *_Nonnull transactions) { - XCTAssertEqual(handler.transactions.count, 0); XCTAssertEqual(transactions.count, 1); [expectation fulfill]; } diff --git a/packages/in_app_purchase/lib/src/store_kit_wrappers/sk_payment_queue_wrapper.dart b/packages/in_app_purchase/lib/src/store_kit_wrappers/sk_payment_queue_wrapper.dart index 33d9281d3ce0..7f20736afbf8 100644 --- a/packages/in_app_purchase/lib/src/store_kit_wrappers/sk_payment_queue_wrapper.dart +++ b/packages/in_app_purchase/lib/src/store_kit_wrappers/sk_payment_queue_wrapper.dart @@ -105,7 +105,7 @@ class SKPaymentQueueWrapper { SKPaymentTransactionWrapper transaction) async { await channel.invokeMethod( '-[InAppPurchasePlugin finishTransaction:result:]', - transaction.payment.productIdentifier); + transaction.transactionIdentifier); } /// Restore previously purchased transactions. diff --git a/packages/in_app_purchase/test/store_kit_wrappers/sk_methodchannel_apis_test.dart b/packages/in_app_purchase/test/store_kit_wrappers/sk_methodchannel_apis_test.dart index c8da68ab823a..3a08d9e8e45d 100644 --- a/packages/in_app_purchase/test/store_kit_wrappers/sk_methodchannel_apis_test.dart +++ b/packages/in_app_purchase/test/store_kit_wrappers/sk_methodchannel_apis_test.dart @@ -110,7 +110,7 @@ void main() { queue.setTransactionObserver(observer); await queue.finishTransaction(dummyTransaction); expect(fakeIOSPlatform.transactionsFinished.first, - equals(dummyTransaction.payment.productIdentifier)); + equals(dummyTransaction.transactionIdentifier)); }); test('should restore transaction', () async {