Skip to content

Commit 6b63ca2

Browse files
kinexLHLL
authored andcommitted
[in_app_purchase] Removed maintaining own cache of transactions (flutter#2911)
* - Removed maintaining own cache of transactions, it is better to use SKPaymentQueue.transactions where needed - Removed unnecessary and broken payment validation from addPayment - Refactored finishTransaction for finishing transactions properly - Fixed: restoreTransactions did not call result(nil) causing the call never complete * - Updated changelog * - Fixed call to finishTransaction: parameter must be transactionIdentifier, not productIdentifier * - review fixes: verify in addPayment there are no pending transactions for the same product * - reverted accidental change * - fixed formatting issues * - fixed formatting issues * - fixed test (removed obsolete references to old transactions cache) * - removed obsolete test testAddPaymentWithSameProductIDWillFail - fixed sk_methodchannel_apis_test * - removed testDuplicateTransactionsWillTriggerAnError Co-authored-by: LHLL <[email protected]>
1 parent 4e26841 commit 6b63ca2

File tree

8 files changed

+29
-142
lines changed

8 files changed

+29
-142
lines changed

packages/in_app_purchase/CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,9 @@
1+
## 0.3.4+8
2+
3+
* [iOS] Fixed: purchase dialog not showing always.
4+
* [iOS] Fixed: completing purchases could fail.
5+
* [iOS] Fixed: restorePurchases caused hang (call never returned).
6+
17
## 0.3.4+7
28

39
* iOS: Fix typo of the `simulatesAskToBuyInSandbox` key.

packages/in_app_purchase/ios/Classes/FIAPaymentQueueHandler.h

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,6 @@ typedef void (^UpdatedDownloads)(NSArray<SKDownload *> *downloads);
1818

1919
@interface FIAPaymentQueueHandler : NSObject <SKPaymentTransactionObserver>
2020

21-
// Unfinished transactions.
22-
@property(nonatomic, readonly)
23-
NSDictionary<NSString *, NSMutableArray<SKPaymentTransaction *> *> *transactions;
24-
2521
- (instancetype)initWithQueue:(nonnull SKPaymentQueue *)queue
2622
transactionsUpdated:(nullable TransactionsUpdated)transactionsUpdated
2723
transactionRemoved:(nullable TransactionsRemoved)transactionsRemoved

packages/in_app_purchase/ios/Classes/FIAPaymentQueueHandler.m

Lines changed: 4 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,6 @@ @interface FIAPaymentQueueHandler ()
1515
@property(nullable, copy, nonatomic) ShouldAddStorePayment shouldAddStorePayment;
1616
@property(nullable, copy, nonatomic) UpdatedDownloads updatedDownloads;
1717

18-
@property(strong, nonatomic)
19-
NSMutableDictionary<NSString *, NSMutableArray<SKPaymentTransaction *> *> *transactionsSetter;
20-
2118
@end
2219

2320
@implementation FIAPaymentQueueHandler
@@ -39,7 +36,6 @@ - (instancetype)initWithQueue:(nonnull SKPaymentQueue *)queue
3936
_paymentQueueRestoreCompletedTransactionsFinished = restoreCompletedTransactionsFinished;
4037
_shouldAddStorePayment = shouldAddStorePayment;
4138
_updatedDownloads = updatedDownloads;
42-
_transactionsSetter = [NSMutableDictionary dictionary];
4339
}
4440
return self;
4541
}
@@ -49,8 +45,10 @@ - (void)startObservingPaymentQueue {
4945
}
5046

5147
- (BOOL)addPayment:(SKPayment *)payment {
52-
if (self.transactionsSetter[payment.productIdentifier]) {
53-
return NO;
48+
for (SKPaymentTransaction *transaction in self.queue.transactions) {
49+
if ([transaction.payment.productIdentifier isEqualToString:payment.productIdentifier]) {
50+
return NO;
51+
}
5452
}
5553
[self.queue addPayment:payment];
5654
return YES;
@@ -74,46 +72,13 @@ - (void)restoreTransactions:(nullable NSString *)applicationName {
7472
// state of transactions and finish as appropriate.
7573
- (void)paymentQueue:(SKPaymentQueue *)queue
7674
updatedTransactions:(NSArray<SKPaymentTransaction *> *)transactions {
77-
for (SKPaymentTransaction *transaction in transactions) {
78-
if (transaction.transactionState != SKPaymentTransactionStatePurchasing) {
79-
// Use product identifier instead of transaction identifier for few reasons:
80-
// 1. Only transactions with purchased state and failed state will have a transaction id, it
81-
// will become impossible for clients to finish deferred transactions when needed.
82-
// 2. Using product identifiers can help prevent clients from purchasing the same
83-
// subscription more than once by accident.
84-
NSMutableArray *transactionArray =
85-
[self.transactionsSetter objectForKey:transaction.payment.productIdentifier];
86-
if (transactionArray == nil) {
87-
transactionArray = [NSMutableArray array];
88-
}
89-
[transactionArray addObject:transaction];
90-
self.transactionsSetter[transaction.payment.productIdentifier] = transactionArray;
91-
}
92-
}
9375
// notify dart through callbacks.
9476
self.transactionsUpdated(transactions);
9577
}
9678

9779
// Sent when transactions are removed from the queue (via finishTransaction:).
9880
- (void)paymentQueue:(SKPaymentQueue *)queue
9981
removedTransactions:(NSArray<SKPaymentTransaction *> *)transactions {
100-
for (SKPaymentTransaction *transaction in transactions) {
101-
NSString *productId = transaction.payment.productIdentifier;
102-
103-
if ([self.transactionsSetter objectForKey:productId] == nil) {
104-
continue;
105-
}
106-
107-
NSPredicate *predicate = [NSPredicate
108-
predicateWithFormat:@"transactionIdentifier == %@", transaction.transactionIdentifier];
109-
NSArray<SKPaymentTransaction *> *filteredTransactions =
110-
[self.transactionsSetter[productId] filteredArrayUsingPredicate:predicate];
111-
[self.transactionsSetter[productId] removeObjectsInArray:filteredTransactions];
112-
113-
if (!self.transactionsSetter[productId] || !self.transactionsSetter[productId].count) {
114-
[self.transactionsSetter removeObjectForKey:productId];
115-
}
116-
}
11782
self.transactionsRemoved(transactions);
11883
}
11984

@@ -146,10 +111,4 @@ - (BOOL)paymentQueue:(SKPaymentQueue *)queue
146111
return self.queue.transactions;
147112
}
148113

149-
#pragma mark - getter
150-
151-
- (NSDictionary<NSString *, NSMutableArray<SKPaymentTransaction *> *> *)transactions {
152-
return [self.transactionsSetter copy];
153-
}
154-
155114
@end

packages/in_app_purchase/ios/Classes/InAppPurchasePlugin.m

Lines changed: 17 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -203,32 +203,24 @@ - (void)finishTransaction:(FlutterMethodCall *)call result:(FlutterResult)result
203203
details:call.arguments]);
204204
return;
205205
}
206-
NSString *identifier = call.arguments;
207-
NSMutableArray *transactions = [self.paymentQueueHandler.transactions objectForKey:identifier];
208-
if (!transactions) {
209-
result([FlutterError
210-
errorWithCode:@"storekit_platform_invalid_transaction"
211-
message:[NSString
212-
stringWithFormat:@"The transaction with transactionIdentifer:%@ does not "
213-
@"exist. Note that if the transactionState is "
214-
@"purchasing, the transactionIdentifier will be "
215-
@"nil(null).",
216-
identifier]
217-
details:call.arguments]);
218-
return;
219-
}
220-
@try {
221-
for (SKPaymentTransaction *transaction in transactions) {
222-
[self.paymentQueueHandler finishTransaction:transaction];
206+
NSString *transactionIdentifier = call.arguments;
207+
208+
NSArray<SKPaymentTransaction *> *pendingTransactions =
209+
[self.paymentQueueHandler getUnfinishedTransactions];
210+
211+
for (SKPaymentTransaction *transaction in pendingTransactions) {
212+
if ([transaction.transactionIdentifier isEqualToString:transactionIdentifier]) {
213+
@try {
214+
[self.paymentQueueHandler finishTransaction:transaction];
215+
} @catch (NSException *e) {
216+
result([FlutterError errorWithCode:@"storekit_finish_transaction_exception"
217+
message:e.name
218+
details:e.description]);
219+
return;
220+
}
223221
}
224-
// finish transaction will throw exception if the transaction type is purchasing. Notify dart
225-
// about this exception.
226-
} @catch (NSException *e) {
227-
result([FlutterError errorWithCode:@"storekit_finish_transaction_exception"
228-
message:e.name
229-
details:e.description]);
230-
return;
231222
}
223+
232224
result(nil);
233225
}
234226

@@ -241,6 +233,7 @@ - (void)restoreTransactions:(FlutterMethodCall *)call result:(FlutterResult)resu
241233
return;
242234
}
243235
[self.paymentQueueHandler restoreTransactions:call.arguments];
236+
result(nil);
244237
}
245238

246239
- (void)retrieveReceiptData:(FlutterMethodCall *)call result:(FlutterResult)result {

packages/in_app_purchase/ios/Tests/InAppPurchasePluginTest.m

Lines changed: 0 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -110,51 +110,6 @@ - (void)testAddPaymentFailure {
110110
XCTAssertEqual(transactionForUpdateBlock.transactionState, SKPaymentTransactionStateFailed);
111111
}
112112

113-
- (void)testAddPaymentWithSameProductIDWillFail {
114-
XCTestExpectation* expectation =
115-
[self expectationWithDescription:@"result should return expected error"];
116-
FlutterMethodCall* call =
117-
[FlutterMethodCall methodCallWithMethodName:@"-[InAppPurchasePlugin addPayment:result:]"
118-
arguments:@{
119-
@"productIdentifier" : @"123",
120-
@"quantity" : @(1),
121-
@"simulatesAskToBuyInSandbox" : @YES,
122-
}];
123-
SKPaymentQueueStub* queue = [SKPaymentQueueStub new];
124-
queue.testState = SKPaymentTransactionStatePurchased;
125-
self.plugin.paymentQueueHandler = [[FIAPaymentQueueHandler alloc] initWithQueue:queue
126-
transactionsUpdated:^(NSArray<SKPaymentTransaction*>* _Nonnull transactions) {
127-
}
128-
transactionRemoved:nil
129-
restoreTransactionFailed:nil
130-
restoreCompletedTransactionsFinished:nil
131-
shouldAddStorePayment:^BOOL(SKPayment* _Nonnull payment, SKProduct* _Nonnull product) {
132-
return YES;
133-
}
134-
updatedDownloads:nil];
135-
[queue addTransactionObserver:self.plugin.paymentQueueHandler];
136-
137-
FlutterResult addDuplicatePaymentBlock = ^(id r) {
138-
XCTAssertNil(r);
139-
[self.plugin
140-
handleMethodCall:call
141-
result:^(id result) {
142-
XCTAssertNotNil(result);
143-
XCTAssertTrue([result isKindOfClass:[FlutterError class]]);
144-
FlutterError* error = (FlutterError*)result;
145-
XCTAssertEqualObjects(error.code, @"storekit_duplicate_product_object");
146-
XCTAssertEqualObjects(
147-
error.message,
148-
@"There is a pending transaction for the same product identifier. Please "
149-
@"either wait for it to be finished or finish it manually using "
150-
@"`completePurchase` to avoid edge cases.");
151-
[expectation fulfill];
152-
}];
153-
};
154-
[self.plugin handleMethodCall:call result:addDuplicatePaymentBlock];
155-
[self waitForExpectations:@[ expectation ] timeout:5];
156-
}
157-
158113
- (void)testAddPaymentSuccessWithMockQueue {
159114
XCTestExpectation* expectation =
160115
[self expectationWithDescription:@"result should return success state"];

packages/in_app_purchase/ios/Tests/PaymentQueueTest.m

Lines changed: 0 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -69,26 +69,6 @@ - (void)testTransactionPurchased {
6969
XCTAssertEqual(tran.transactionIdentifier, @"fakeID");
7070
}
7171

72-
- (void)testDuplicateTransactionsWillTriggerAnError {
73-
SKPaymentQueueStub *queue = [[SKPaymentQueueStub alloc] init];
74-
queue.testState = SKPaymentTransactionStatePurchased;
75-
FIAPaymentQueueHandler *handler = [[FIAPaymentQueueHandler alloc] initWithQueue:queue
76-
transactionsUpdated:^(NSArray<SKPaymentTransaction *> *_Nonnull transactions) {
77-
}
78-
transactionRemoved:nil
79-
restoreTransactionFailed:nil
80-
restoreCompletedTransactionsFinished:nil
81-
shouldAddStorePayment:^BOOL(SKPayment *_Nonnull payment, SKProduct *_Nonnull product) {
82-
return YES;
83-
}
84-
updatedDownloads:nil];
85-
[queue addTransactionObserver:handler];
86-
SKPayment *payment =
87-
[SKPayment paymentWithProduct:[[SKProductStub alloc] initWithMap:self.productResponseMap]];
88-
XCTAssertTrue([handler addPayment:payment]);
89-
XCTAssertFalse([handler addPayment:payment]);
90-
}
91-
9272
- (void)testTransactionFailed {
9373
XCTestExpectation *expectation =
9474
[self expectationWithDescription:@"expect to get failed transcation."];
@@ -208,13 +188,11 @@ - (void)testFinishTransaction {
208188
queue.testState = SKPaymentTransactionStateDeferred;
209189
__block FIAPaymentQueueHandler *handler = [[FIAPaymentQueueHandler alloc] initWithQueue:queue
210190
transactionsUpdated:^(NSArray<SKPaymentTransaction *> *_Nonnull transactions) {
211-
XCTAssertEqual(handler.transactions.count, 1);
212191
XCTAssertEqual(transactions.count, 1);
213192
SKPaymentTransaction *transaction = transactions[0];
214193
[handler finishTransaction:transaction];
215194
}
216195
transactionRemoved:^(NSArray<SKPaymentTransaction *> *_Nonnull transactions) {
217-
XCTAssertEqual(handler.transactions.count, 0);
218196
XCTAssertEqual(transactions.count, 1);
219197
[expectation fulfill];
220198
}

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -105,7 +105,7 @@ class SKPaymentQueueWrapper {
105105
SKPaymentTransactionWrapper transaction) async {
106106
await channel.invokeMethod<void>(
107107
'-[InAppPurchasePlugin finishTransaction:result:]',
108-
transaction.payment.productIdentifier);
108+
transaction.transactionIdentifier);
109109
}
110110

111111
/// Restore previously purchased transactions.

packages/in_app_purchase/test/store_kit_wrappers/sk_methodchannel_apis_test.dart

Lines changed: 1 addition & 1 deletion
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.payment.productIdentifier));
113+
equals(dummyTransaction.transactionIdentifier));
114114
});
115115

116116
test('should restore transaction', () async {

0 commit comments

Comments
 (0)