Skip to content
This repository was archived by the owner on Feb 22, 2023. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions packages/in_app_purchase/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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.
Expand Down
4 changes: 0 additions & 4 deletions packages/in_app_purchase/ios/Classes/FIAPaymentQueueHandler.h
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,6 @@ typedef void (^UpdatedDownloads)(NSArray<SKDownload *> *downloads);

@interface FIAPaymentQueueHandler : NSObject <SKPaymentTransactionObserver>

// Unfinished transactions.
@property(nonatomic, readonly)
NSDictionary<NSString *, NSMutableArray<SKPaymentTransaction *> *> *transactions;

- (instancetype)initWithQueue:(nonnull SKPaymentQueue *)queue
transactionsUpdated:(nullable TransactionsUpdated)transactionsUpdated
transactionRemoved:(nullable TransactionsRemoved)transactionsRemoved
Expand Down
49 changes: 4 additions & 45 deletions packages/in_app_purchase/ios/Classes/FIAPaymentQueueHandler.m
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,6 @@ @interface FIAPaymentQueueHandler ()
@property(nullable, copy, nonatomic) ShouldAddStorePayment shouldAddStorePayment;
@property(nullable, copy, nonatomic) UpdatedDownloads updatedDownloads;

@property(strong, nonatomic)
NSMutableDictionary<NSString *, NSMutableArray<SKPaymentTransaction *> *> *transactionsSetter;

@end

@implementation FIAPaymentQueueHandler
Expand All @@ -39,7 +36,6 @@ - (instancetype)initWithQueue:(nonnull SKPaymentQueue *)queue
_paymentQueueRestoreCompletedTransactionsFinished = restoreCompletedTransactionsFinished;
_shouldAddStorePayment = shouldAddStorePayment;
_updatedDownloads = updatedDownloads;
_transactionsSetter = [NSMutableDictionary dictionary];
}
return self;
}
Expand All @@ -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;
Expand All @@ -74,46 +72,13 @@ - (void)restoreTransactions:(nullable NSString *)applicationName {
// state of transactions and finish as appropriate.
- (void)paymentQueue:(SKPaymentQueue *)queue
updatedTransactions:(NSArray<SKPaymentTransaction *> *)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);
}

// Sent when transactions are removed from the queue (via finishTransaction:).
- (void)paymentQueue:(SKPaymentQueue *)queue
removedTransactions:(NSArray<SKPaymentTransaction *> *)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<SKPaymentTransaction *> *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);
}

Expand Down Expand Up @@ -146,10 +111,4 @@ - (BOOL)paymentQueue:(SKPaymentQueue *)queue
return self.queue.transactions;
}

#pragma mark - getter

- (NSDictionary<NSString *, NSMutableArray<SKPaymentTransaction *> *> *)transactions {
return [self.transactionsSetter copy];
}

@end
41 changes: 17 additions & 24 deletions packages/in_app_purchase/ios/Classes/InAppPurchasePlugin.m
Original file line number Diff line number Diff line change
Expand Up @@ -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<SKPaymentTransaction *> *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);
}

Expand All @@ -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 {
Expand Down
45 changes: 0 additions & 45 deletions packages/in_app_purchase/ios/Tests/InAppPurchasePluginTest.m
Original file line number Diff line number Diff line change
Expand Up @@ -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<SKPaymentTransaction*>* _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"];
Expand Down
22 changes: 0 additions & 22 deletions packages/in_app_purchase/ios/Tests/PaymentQueueTest.m
Original file line number Diff line number Diff line change
Expand Up @@ -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<SKPaymentTransaction *> *_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."];
Expand Down Expand Up @@ -208,13 +188,11 @@ - (void)testFinishTransaction {
queue.testState = SKPaymentTransactionStateDeferred;
__block FIAPaymentQueueHandler *handler = [[FIAPaymentQueueHandler alloc] initWithQueue:queue
transactionsUpdated:^(NSArray<SKPaymentTransaction *> *_Nonnull transactions) {
XCTAssertEqual(handler.transactions.count, 1);
XCTAssertEqual(transactions.count, 1);
SKPaymentTransaction *transaction = transactions[0];
[handler finishTransaction:transaction];
}
transactionRemoved:^(NSArray<SKPaymentTransaction *> *_Nonnull transactions) {
XCTAssertEqual(handler.transactions.count, 0);
XCTAssertEqual(transactions.count, 1);
[expectation fulfill];
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@ class SKPaymentQueueWrapper {
SKPaymentTransactionWrapper transaction) async {
await channel.invokeMethod<void>(
'-[InAppPurchasePlugin finishTransaction:result:]',
transaction.payment.productIdentifier);
transaction.transactionIdentifier);
}

/// Restore previously purchased transactions.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down