Skip to content
This repository was archived by the owner on Feb 22, 2023. It is now read-only.
Merged
5 changes: 5 additions & 0 deletions packages/in_app_purchase/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
## 0.3.4+1

* iOS: Fix the bug that `SKPaymentQueueWrapper.transactions` doesn't return all transactions.
* iOS: Fix the app crashes if `InAppPurchaseConnection.instance` is called in the `main()`.

## 0.3.4

* Expose SKError code to client apps.
Expand Down
6 changes: 2 additions & 4 deletions packages/in_app_purchase/ios/Classes/FIAPaymentQueueHandler.m
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ - (void)restoreTransactions:(nullable NSString *)applicationName {
- (void)paymentQueue:(SKPaymentQueue *)queue
updatedTransactions:(NSArray<SKPaymentTransaction *> *)transactions {
for (SKPaymentTransaction *transaction in transactions) {
if (transaction.transactionIdentifier) {
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.
Expand All @@ -92,9 +92,7 @@ - (void)paymentQueue:(SKPaymentQueue *)queue
- (void)paymentQueue:(SKPaymentQueue *)queue
removedTransactions:(NSArray<SKPaymentTransaction *> *)transactions {
for (SKPaymentTransaction *transaction in transactions) {
if (transaction.transactionIdentifier) {
[self.transactionsSetter removeObjectForKey:transaction.payment.productIdentifier];
}
[self.transactionsSetter removeObjectForKey:transaction.payment.productIdentifier];
}
self.transactionsRemoved(transactions);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,7 @@ - (void)testAddPaymentWithSameProductIDWillFail {
@"simulatesAskToBuyInSandBox" : @YES,
}];
SKPaymentQueueStub* queue = [SKPaymentQueueStub new];
queue.testState = SKPaymentTransactionStatePurchased;
self.plugin.paymentQueueHandler = [[FIAPaymentQueueHandler alloc] initWithQueue:queue
transactionsUpdated:^(NSArray<SKPaymentTransaction*>* _Nonnull transactions) {
}
Expand Down
5 changes: 5 additions & 0 deletions packages/in_app_purchase/ios/Tests/PaymentQueueTest.m
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ - (void)testTransactionPurchased {
[handler addPayment:payment];
[self waitForExpectations:@[ expectation ] timeout:5];
XCTAssertEqual(tran.transactionState, SKPaymentTransactionStatePurchased);
XCTAssertEqual(tran.transactionIdentifier, @"fakeID");
}

- (void)testDuplicateTransactionsWillTriggerAnError {
Expand Down Expand Up @@ -113,6 +114,7 @@ - (void)testTransactionFailed {
[handler addPayment:payment];
[self waitForExpectations:@[ expectation ] timeout:5];
XCTAssertEqual(tran.transactionState, SKPaymentTransactionStateFailed);
XCTAssertEqual(tran.transactionIdentifier, nil);
}

- (void)testTransactionRestored {
Expand Down Expand Up @@ -140,6 +142,7 @@ - (void)testTransactionRestored {
[handler addPayment:payment];
[self waitForExpectations:@[ expectation ] timeout:5];
XCTAssertEqual(tran.transactionState, SKPaymentTransactionStateRestored);
XCTAssertEqual(tran.transactionIdentifier, @"fakeID");
}

- (void)testTransactionPurchasing {
Expand Down Expand Up @@ -167,6 +170,7 @@ - (void)testTransactionPurchasing {
[handler addPayment:payment];
[self waitForExpectations:@[ expectation ] timeout:5];
XCTAssertEqual(tran.transactionState, SKPaymentTransactionStatePurchasing);
XCTAssertEqual(tran.transactionIdentifier, nil);
}

- (void)testTransactionDeferred {
Expand Down Expand Up @@ -194,6 +198,7 @@ - (void)testTransactionDeferred {
[handler addPayment:payment];
[self waitForExpectations:@[ expectation ] timeout:5];
XCTAssertEqual(tran.transactionState, SKPaymentTransactionStateDeferred);
XCTAssertEqual(tran.transactionIdentifier, nil);
}

- (void)testFinishTransaction {
Expand Down
12 changes: 10 additions & 2 deletions packages/in_app_purchase/ios/Tests/Stubs.m
Original file line number Diff line number Diff line change
Expand Up @@ -215,7 +215,11 @@ - (instancetype)initWithMap:(NSDictionary *)map {
- (instancetype)initWithState:(SKPaymentTransactionState)state {
self = [super init];
if (self) {
[self setValue:@"fakeID" forKey:@"transactionIdentifier"];
// Only purchased and restored transactions have transactionIdentifier:
// https://developer.apple.com/documentation/storekit/skpaymenttransaction/1411288-transactionidentifier?language=objc
if (state == SKPaymentTransactionStatePurchased || state == SKPaymentTransactionStateRestored) {
[self setValue:@"fakeID" forKey:@"transactionIdentifier"];
}
[self setValue:@(state) forKey:@"transactionState"];
}
return self;
Expand All @@ -224,7 +228,11 @@ - (instancetype)initWithState:(SKPaymentTransactionState)state {
- (instancetype)initWithState:(SKPaymentTransactionState)state payment:(SKPayment *)payment {
self = [super init];
if (self) {
[self setValue:@"fakeID" forKey:@"transactionIdentifier"];
// Only purchased and restored transactions have transactionIdentifier:
// https://developer.apple.com/documentation/storekit/skpaymenttransaction/1411288-transactionidentifier?language=objc
if (state == SKPaymentTransactionStatePurchased || state == SKPaymentTransactionStateRestored) {
[self setValue:@"fakeID" forKey:@"transactionIdentifier"];
}
[self setValue:@(state) forKey:@"transactionState"];
_payment = payment;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -208,12 +208,14 @@ class _TransactionObserver implements SKTransactionObserverWrapper {
return wrapper.transactionState ==
SKPaymentTransactionStateWrapper.restored;
}).map((SKPaymentTransactionWrapper wrapper) => wrapper));
return;
}

String receiptData = await getReceiptData();
purchaseUpdatedController
.add(transactions.map((SKPaymentTransactionWrapper transaction) {
.add(transactions.where((SKPaymentTransactionWrapper wrapper) {
return wrapper.transactionState !=
SKPaymentTransactionStateWrapper.restored;
}).map((SKPaymentTransactionWrapper transaction) {
Comment on lines +215 to +218
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@LHLL and @cyanglaz

Make sure queryPastPurchases() won't block transaction updates. When the client apps calls queryPastPurchases() right after app launch, they should still receive transaction updates in the purchaseUpdatedStream for transactions with state != restored.

Why did you add this filter and only give transactions that are not restored? Seems like this has resulted in a different bug, see: flutter/flutter#43957 (comment)

PurchaseDetails purchaseDetails =
PurchaseDetails.fromSKTransaction(transaction, receiptData);
return purchaseDetails;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ abstract class InAppPurchaseConnection {

/// Enable the [InAppPurchaseConnection] to handle pending purchases.
///
/// Android Only: This method is required to be called when initialize the application.
/// This method is required to be called when initialize the application.
/// It is to acknowledge your application has been updated to support pending purchases.
/// See [Support pending transactions](https://developer.android.com/google/play/billing/billing_library_overview#pending)
/// for more details.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,9 +37,7 @@ class SKPaymentQueueWrapper {

static final SKPaymentQueueWrapper _singleton = SKPaymentQueueWrapper._();

SKPaymentQueueWrapper._() {
callbackChannel.setMethodCallHandler(_handleObserverCallbacks);
}
SKPaymentQueueWrapper._();

/// Calls [`-[SKPaymentQueue transactions]`](https://developer.apple.com/documentation/storekit/skpaymentqueue/1506026-transactions?language=objc)
Future<List<SKPaymentTransactionWrapper>> transactions() async {
Expand All @@ -59,6 +57,7 @@ class SKPaymentQueueWrapper {
/// addTransactionObserver:]`](https://developer.apple.com/documentation/storekit/skpaymentqueue/1506042-addtransactionobserver?language=objc).
void setTransactionObserver(SKTransactionObserverWrapper observer) {
_observer = observer;
callbackChannel.setMethodCallHandler(_handleObserverCallbacks);
}

/// Posts a payment to the queue.
Expand Down
2 changes: 1 addition & 1 deletion packages/in_app_purchase/pubspec.yaml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
name: in_app_purchase
description: A Flutter plugin for in-app purchases. Exposes APIs for making in-app purchases through the App Store and Google Play.
homepage: https://github.com/flutter/plugins/tree/master/packages/in_app_purchase
version: 0.3.4
version: 0.3.4+1

dependencies:
async: ^2.0.8
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,28 @@ void main() {
expect(response.error, isNull);
});

test('queryPastPurchases should not block transaction updates', () async {
fakeIOSPlatform.transactions
.add(fakeIOSPlatform.createPurchasedTransactionWithProductID('foo'));
Completer completer = Completer();
Stream<List<PurchaseDetails>> stream =
AppStoreConnection.instance.purchaseUpdatedStream;

StreamSubscription subscription;
subscription = stream.listen((purchaseDetailsList) {
if (purchaseDetailsList.first.status == PurchaseStatus.purchased) {
completer.complete(purchaseDetailsList);
subscription.cancel();
}
});
QueryPurchaseDetailsResponse response =
await AppStoreConnection.instance.queryPastPurchases();
List<PurchaseDetails> result = await completer.future;
expect(result.length, 1);
expect(result.first.productID, 'foo');
expect(response.error, isNull);
});

test('should get empty result if there is no restored transactions',
() async {
fakeIOSPlatform.testRestoredTransactionsNull = true;
Expand Down Expand Up @@ -328,10 +350,10 @@ class FakeIOSPlatform {

SKPaymentTransactionWrapper createPendingTransactionWithProductID(String id) {
return SKPaymentTransactionWrapper(
transactionIdentifier: null,
payment: SKPaymentWrapper(productIdentifier: id),
transactionState: SKPaymentTransactionStateWrapper.purchasing,
transactionTimeStamp: 123123.121,
transactionIdentifier: id,
error: null,
originalTransaction: null);
}
Expand All @@ -349,10 +371,10 @@ class FakeIOSPlatform {

SKPaymentTransactionWrapper createFailedTransactionWithProductID(String id) {
return SKPaymentTransactionWrapper(
transactionIdentifier: null,
payment: SKPaymentWrapper(productIdentifier: id),
transactionState: SKPaymentTransactionStateWrapper.failed,
transactionTimeStamp: 123123.121,
transactionIdentifier: id,
error: SKError(
code: 0,
domain: 'ios_domain',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ final SKPaymentTransactionWrapper dummyOriginalTransaction =
transactionIdentifier: '123123',
error: dummyError,
);

final SKPaymentTransactionWrapper dummyTransaction =
SKPaymentTransactionWrapper(
transactionState: SKPaymentTransactionStateWrapper.purchased,
Expand Down