Skip to content
Draft
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
Original file line number Diff line number Diff line change
@@ -1,3 +1,11 @@
## 0.4.11

* Adds latestTransaction to SK2Transaction wrapper and InAppPurchaseStoreKitPlatformAddition.

## 0.4.10+1

* Fixes SK2Transaction to expose the real purchased quantity instead of defaulting to 1.

## 0.4.10

* Clarifies `completePurchase` usage and the consequences of unfinished transactions in the README and API docstrings.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -418,6 +418,29 @@ extension InAppPurchasePlugin: InAppPurchase2API {
}
}

func latestTransaction(
productId: String, completion: @escaping (Result<SK2TransactionMessage?, Error>) -> Void
) {
Task {
let verificationResult = await Transaction.latest(for: productId)
guard let verificationResult = verificationResult else {
completion(.success(nil))
return
}
let transaction: Transaction
switch verificationResult {
case .verified(let verifiedTransaction):
transaction = verifiedTransaction
case .unverified(let unverifiedTransaction, _):
transaction = unverifiedTransaction
}
completion(
.success(
transaction.convertToPigeon(
receipt: verificationResult.jwsRepresentation, status: .purchased)))
}
}

/// This Task listens to Transation.updates as shown here
/// https://developer.apple.com/documentation/storekit/transaction/3851206-updates
/// This function should be called as soon as the app starts to avoid missing any Transactions done outside of the app.
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// Copyright 2013 The Flutter Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
// Autogenerated from Pigeon (v26.1.10), do not edit directly.
// Autogenerated from Pigeon (v26.2.0), do not edit directly.
// See also: https://pub.dev/packages/pigeon

import Foundation
Expand Down Expand Up @@ -760,6 +760,8 @@ protocol InAppPurchase2API {
func countryCode(completion: @escaping (Result<String, Error>) -> Void)
func sync(completion: @escaping (Result<Void, Error>) -> Void)
func presentOfferCodeRedeemSheet(completion: @escaping (Result<Void, Error>) -> Void)
func latestTransaction(
productId: String, completion: @escaping (Result<SK2TransactionMessage?, Error>) -> Void)
}

/// Generated setup class from Pigeon to handle messages through the `binaryMessenger`.
Expand Down Expand Up @@ -1027,6 +1029,26 @@ class InAppPurchase2APISetup {
} else {
presentOfferCodeRedeemSheetChannel.setMessageHandler(nil)
}
let latestTransactionChannel = FlutterBasicMessageChannel(
name:
"dev.flutter.pigeon.in_app_purchase_storekit.InAppPurchase2API.latestTransaction\(channelSuffix)",
binaryMessenger: binaryMessenger, codec: codec)
if let api = api {
latestTransactionChannel.setMessageHandler { message, reply in
let args = message as! [Any?]
let productIdArg = args[0] as! String
api.latestTransaction(productId: productIdArg) { result in
switch result {
case .success(let res):
reply(wrapResult(res))
case .failure(let error):
reply(wrapError(error))
}
}
}
} else {
latestTransactionChannel.setMessageHandler(nil)
}
}
}
/// Generated protocol from Pigeon that represents Flutter messages that can be called from Swift.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -733,11 +733,11 @@ void SetUpFIAInAppPurchaseAPIWithSuffix(id<FlutterBinaryMessenger> binaryMesseng
binaryMessenger:binaryMessenger
codec:FIAGetMessagesCodec()];
if (api) {
NSCAssert([api respondsToSelector:@selector(startProductRequestProductIdentifiers:
completion:)],
@"FIAInAppPurchaseAPI api (%@) doesn't respond to "
@"@selector(startProductRequestProductIdentifiers:completion:)",
api);
NSCAssert(
[api respondsToSelector:@selector(startProductRequestProductIdentifiers:completion:)],
@"FIAInAppPurchaseAPI api (%@) doesn't respond to "
@"@selector(startProductRequestProductIdentifiers:completion:)",
api);
[channel setMessageHandler:^(id _Nullable message, FlutterReply callback) {
NSArray<id> *args = message;
NSArray<NSString *> *arg_productIdentifiers = GetNullableObjectAtIndex(args, 0);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,13 @@ class InAppPurchaseStoreKitPlatformAddition extends InAppPurchasePlatformAdditio
return AppStore().sync();
}

/// Gets the customer's most recent transaction for a product.
///
/// This is only supported when StoreKit 2 is enabled.
Future<SK2Transaction?> latestTransaction(String productId) {
return SK2Transaction.latestTransaction(productId);
}

/// Present Code Redemption Sheet.
///
/// Available on devices running iOS 14 and iPadOS 14 and later.
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// Copyright 2013 The Flutter Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
// Autogenerated from Pigeon (v26.1.10), do not edit directly.
// Autogenerated from Pigeon (v26.2.0), do not edit directly.
// See also: https://pub.dev/packages/pigeon
// ignore_for_file: public_member_api_docs, non_constant_identifier_names, avoid_as, unused_import, unnecessary_parenthesis, prefer_null_aware_operators, omit_local_variable_types, omit_obvious_local_variable_types, unused_shown_name, unnecessary_import, no_leading_underscores_for_local_identifiers

Expand Down Expand Up @@ -1023,6 +1023,25 @@ class InAppPurchase2API {

_extractReplyValueOrThrow(pigeonVar_replyList, pigeonVar_channelName, isNullValid: true);
}

Future<SK2TransactionMessage?> latestTransaction(String productId) async {
final pigeonVar_channelName =
'dev.flutter.pigeon.in_app_purchase_storekit.InAppPurchase2API.latestTransaction$pigeonVar_messageChannelSuffix';
final pigeonVar_channel = BasicMessageChannel<Object?>(
pigeonVar_channelName,
pigeonChannelCodec,
binaryMessenger: pigeonVar_binaryMessenger,
);
final Future<Object?> pigeonVar_sendFuture = pigeonVar_channel.send(<Object?>[productId]);
final pigeonVar_replyList = await pigeonVar_sendFuture as List<Object?>?;

final Object? pigeonVar_replyValue = _extractReplyValueOrThrow(
pigeonVar_replyList,
pigeonVar_channelName,
isNullValid: true,
);
return pigeonVar_replyValue as SK2TransactionMessage?;
}
}

abstract class InAppPurchase2CallbackAPI {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,14 @@ class SK2Transaction {
static Future<void> restorePurchases() async {
await hostApi2.restorePurchases();
}

/// A wrapper around [Transaction.latest(for:)]
/// https://developer.apple.com/documentation/storekit/transaction-latest_for__
/// Gets the customer's most recent transaction for an In-App Purchase.
static Future<SK2Transaction?> latestTransaction(String productId) async {
final SK2TransactionMessage? msg = await hostApi2.latestTransaction(productId);
return msg?.convertFromPigeon();
}
}

extension on SK2TransactionMessage {
Expand All @@ -126,6 +134,7 @@ extension on SK2TransactionMessage {
productId: productId,
purchaseDate: purchaseDate ?? '',
expirationDate: expirationDate,
quantity: purchasedQuantity,
appAccountToken: appAccountToken,
receiptData: receiptData,
jsonRepresentation: jsonRepresentation,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -261,6 +261,9 @@ abstract class InAppPurchase2API {

@async
void presentOfferCodeRedeemSheet();

@async
SK2TransactionMessage? latestTransaction(String productId);
}

@FlutterApi()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ name: in_app_purchase_storekit
description: An implementation for the iOS and macOS platforms of the Flutter `in_app_purchase` plugin. This uses the StoreKit Framework.
repository: https://github.com/flutter/packages/tree/main/packages/in_app_purchase/in_app_purchase_storekit
issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+in_app_purchase%22
version: 0.4.10
version: 0.4.11

environment:
sdk: ^3.10.0
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -319,6 +319,8 @@ class FakeStoreKit2Platform implements InAppPurchase2API {
late bool testTransactionFail;
late int testTransactionCancel;
late List<SK2Transaction> finishedTransactions;
List<SK2TransactionMessage> transactionsList = <SK2TransactionMessage>[];
List<SK2TransactionMessage> unfinishedTransactionsList = <SK2TransactionMessage>[];

PlatformException? queryProductException;
bool isListenerRegistered = false;
Expand Down Expand Up @@ -348,6 +350,28 @@ class FakeStoreKit2Platform implements InAppPurchase2API {
eligibleWinBackOffers = <String, Set<String>>{};
eligibleIntroductoryOffers = <String, bool>{};
simulatedPurchaseResult = SK2ProductPurchaseResultMessage.success;
transactionsList = <SK2TransactionMessage>[
SK2TransactionMessage(
id: 123,
originalId: 123,
productId: 'product_id',
purchaseDate: '12-12',
purchasedQuantity: 2,
status: SK2PurchaseStatusMessage.purchased,
),
];
unfinishedTransactionsList = <SK2TransactionMessage>[
SK2TransactionMessage(
id: 123,
originalId: 123,
productId: 'product_id',
purchaseDate: '12-12',
receiptData: 'fake_jws_representation',
appAccountToken: 'fake_app_account_token',
purchasedQuantity: 3,
status: SK2PurchaseStatusMessage.purchased,
),
];
}

SK2TransactionMessage createRestoredTransaction(
Expand Down Expand Up @@ -449,30 +473,12 @@ class FakeStoreKit2Platform implements InAppPurchase2API {

@override
Future<List<SK2TransactionMessage>> transactions() {
return Future<List<SK2TransactionMessage>>.value(<SK2TransactionMessage>[
SK2TransactionMessage(
id: 123,
originalId: 123,
productId: 'product_id',
purchaseDate: '12-12',
status: SK2PurchaseStatusMessage.purchased,
),
]);
return Future<List<SK2TransactionMessage>>.value(transactionsList);
}

@override
Future<List<SK2TransactionMessage>> unfinishedTransactions() {
return Future<List<SK2TransactionMessage>>.value(<SK2TransactionMessage>[
SK2TransactionMessage(
id: 123,
originalId: 123,
productId: 'product_id',
purchaseDate: '12-12',
receiptData: 'fake_jws_representation',
appAccountToken: 'fake_app_account_token',
status: SK2PurchaseStatusMessage.purchased,
),
]);
return Future<List<SK2TransactionMessage>>.value(unfinishedTransactionsList);
}

@override
Expand Down Expand Up @@ -550,6 +556,16 @@ class FakeStoreKit2Platform implements InAppPurchase2API {

@override
Future<void> presentOfferCodeRedeemSheet() async {}

@override
Future<SK2TransactionMessage?> latestTransaction(String productId) async {
for (final SK2TransactionMessage tx in transactionsList) {
if (tx.productId == productId) {
return tx;
}
}
return null;
}
}

SK2TransactionMessage createPendingTransaction(String id, {int quantity = 1}) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -655,5 +655,52 @@ void main() {
expect(transactions.first.appAccountToken, isNotNull);
expect(transactions.first.appAccountToken, 'fake_app_account_token');
});

test('should expose purchased quantity in unfinished transactions', () async {
final List<SK2Transaction> transactions = await SK2Transaction.unfinishedTransactions();

expect(transactions, isNotEmpty);
expect(transactions.first.quantity, 3);
});
});

group('transactions', () {
test('should return transactions', () async {
final List<SK2Transaction> transactions = await SK2Transaction.transactions();

expect(transactions, isNotEmpty);
expect(transactions.first.id, '123');
expect(transactions.first.productId, 'product_id');
expect(transactions.first.quantity, 2);
});
});

group('latestTransaction', () {
test('should return latest transaction when product has one', () async {
final SK2Transaction? transaction = await SK2Transaction.latestTransaction('product_id');

expect(transaction, isNotNull);
expect(transaction!.id, '123');
expect(transaction.productId, 'product_id');
expect(transaction.quantity, 2);
});

test('should return null when product has no transactions', () async {
final SK2Transaction? transaction = await SK2Transaction.latestTransaction(
'non_existent_product',
);

expect(transaction, isNull);
});

test('should return latest transaction via platform addition', () async {
final addition =
InAppPurchasePlatformAddition.instance! as InAppPurchaseStoreKitPlatformAddition;
final SK2Transaction? transaction = await addition.latestTransaction('product_id');

expect(transaction, isNotNull);
expect(transaction!.id, '123');
expect(transaction.productId, 'product_id');
});
});
}
Loading