Skip to content
Open
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,8 @@
## 0.4.7

* Adds `SK2Transaction.unfinishedTransactions()` method to query only unfinished transactions.
* Exposes `appAccountToken` property in `SK2PurchaseDetails` for user identification.

## 0.4.6+2

* Updates to Pigeon 26.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -230,6 +230,30 @@ extension InAppPurchasePlugin: InAppPurchase2API {
}
}

/// Wrapper method around StoreKit2's Transaction.unfinished
/// https://developer.apple.com/documentation/storekit/transaction/unfinished
func unfinishedTransactions(
completion: @escaping (Result<[SK2TransactionMessage], Error>) -> Void
) {
Task {
@MainActor in
do {
var transactionsMsgs: [SK2TransactionMessage] = []
for await verificationResult in Transaction.unfinished {
switch verificationResult {
case .verified(let transaction):
transactionsMsgs.append(
transaction.convertToPigeon(receipt: verificationResult.jwsRepresentation)
)
case .unverified:
break
}
}
completion(.success(transactionsMsgs))
}
}
}

func restorePurchases(completion: @escaping (Result<Void, Error>) -> Void) {
Task { [weak self] in
guard let self = self else { return }
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.0), do not edit directly.
// Autogenerated from Pigeon (v26.1.1), do not edit directly.
// See also: https://pub.dev/packages/pigeon

import Foundation
Expand Down Expand Up @@ -727,6 +727,8 @@ protocol InAppPurchase2API {
func isIntroductoryOfferEligible(
productId: String, completion: @escaping (Result<Bool, Error>) -> Void)
func transactions(completion: @escaping (Result<[SK2TransactionMessage], Error>) -> Void)
func unfinishedTransactions(
completion: @escaping (Result<[SK2TransactionMessage], Error>) -> Void)
func finish(id: Int64, completion: @escaping (Result<Void, Error>) -> Void)
func startListeningToTransactions() throws
func stopListeningToTransactions() throws
Expand Down Expand Up @@ -860,6 +862,24 @@ class InAppPurchase2APISetup {
} else {
transactionsChannel.setMessageHandler(nil)
}
let unfinishedTransactionsChannel = FlutterBasicMessageChannel(
name:
"dev.flutter.pigeon.in_app_purchase_storekit.InAppPurchase2API.unfinishedTransactions\(channelSuffix)",
binaryMessenger: binaryMessenger, codec: codec)
if let api = api {
unfinishedTransactionsChannel.setMessageHandler { _, reply in
api.unfinishedTransactions { result in
switch result {
case .success(let res):
reply(wrapResult(res))
case .failure(let error):
reply(wrapError(error))
}
}
}
} else {
unfinishedTransactionsChannel.setMessageHandler(nil)
}
let finishChannel = FlutterBasicMessageChannel(
name: "dev.flutter.pigeon.in_app_purchase_storekit.InAppPurchase2API.finish\(channelSuffix)",
binaryMessenger: binaryMessenger, codec: codec)
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.0), do not edit directly.
// Autogenerated from Pigeon (v26.1.1), 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, unused_shown_name, unnecessary_import, no_leading_underscores_for_local_identifiers

Expand Down Expand Up @@ -978,6 +978,37 @@ class InAppPurchase2API {
}
}

Future<List<SK2TransactionMessage>> unfinishedTransactions() async {
final String pigeonVar_channelName =
'dev.flutter.pigeon.in_app_purchase_storekit.InAppPurchase2API.unfinishedTransactions$pigeonVar_messageChannelSuffix';
final BasicMessageChannel<Object?> pigeonVar_channel =
BasicMessageChannel<Object?>(
pigeonVar_channelName,
pigeonChannelCodec,
binaryMessenger: pigeonVar_binaryMessenger,
);
final Future<Object?> pigeonVar_sendFuture = pigeonVar_channel.send(null);
final List<Object?>? pigeonVar_replyList =
await pigeonVar_sendFuture as List<Object?>?;
if (pigeonVar_replyList == null) {
throw _createConnectionError(pigeonVar_channelName);
} else if (pigeonVar_replyList.length > 1) {
throw PlatformException(
code: pigeonVar_replyList[0]! as String,
message: pigeonVar_replyList[1] as String?,
details: pigeonVar_replyList[2],
);
} else if (pigeonVar_replyList[0] == null) {
throw PlatformException(
code: 'null-error',
message: 'Host platform returned null value for non-null return value.',
);
} else {
return (pigeonVar_replyList[0] as List<Object?>?)!
.cast<SK2TransactionMessage>();
}
}

Future<void> finish(int id) async {
final String pigeonVar_channelName =
'dev.flutter.pigeon.in_app_purchase_storekit.InAppPurchase2API.finish$pigeonVar_messageChannelSuffix';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ class SK2Transaction {
this.subscriptionGroupID,
this.price,
this.error,
this.receiptData,
this.jsonRepresentation,
});

Expand Down Expand Up @@ -63,7 +64,11 @@ class SK2Transaction {
/// Any error returned from StoreKit
final SKError? error;

/// The json representation of a transaction
/// The JWS (JSON Web Signature) representation of the transaction.
/// This is the jwsRepresentation from StoreKit used for server-side verification.
final String? receiptData;

/// The json representation of a transaction.
final String? jsonRepresentation;

/// Wrapper around [Transaction.finish]
Expand All @@ -76,7 +81,7 @@ class SK2Transaction {

/// A wrapper around [Transaction.all]
/// https://developer.apple.com/documentation/storekit/transaction/3851203-all
/// A sequence that emits all the customers transactions for your app.
/// A sequence that emits all the customer's transactions for your app.
static Future<List<SK2Transaction>> transactions() async {
final List<SK2TransactionMessage> msgs = await hostApi2.transactions();
final List<SK2Transaction> transactions = msgs
Expand All @@ -85,6 +90,18 @@ class SK2Transaction {
return transactions;
}

/// A wrapper around [Transaction.unfinished]
/// https://developer.apple.com/documentation/storekit/transaction/unfinished
/// A sequence that emits unfinished transactions for the customer.
static Future<List<SK2Transaction>> unfinishedTransactions() async {
final List<SK2TransactionMessage> msgs = await hostApi2
.unfinishedTransactions();
final List<SK2Transaction> transactions = msgs
.map((SK2TransactionMessage e) => e.convertFromPigeon())
.toList();
return transactions;
}

/// Start listening to transactions.
/// Call this as soon as you can your app to avoid missing transactions.
static void startListeningToTransactions() {
Expand All @@ -111,6 +128,7 @@ extension on SK2TransactionMessage {
purchaseDate: purchaseDate,
expirationDate: expirationDate,
appAccountToken: appAccountToken,
receiptData: receiptData,
jsonRepresentation: jsonRepresentation,
);
}
Expand All @@ -135,6 +153,7 @@ extension on SK2TransactionMessage {
// Any failed transaction will simply not be returned.
status: restoring ? PurchaseStatus.restored : PurchaseStatus.purchased,
purchaseID: id.toString(),
appAccountToken: appAccountToken,
);
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -91,8 +91,13 @@ class SK2PurchaseDetails extends PurchaseDetails {
required super.verificationData,
required super.transactionDate,
required super.status,
this.appAccountToken,
});

/// A UUID that associates the transaction with a user on your own service.
/// This is the value set when making the purchase via appAccountToken option.
final String? appAccountToken;

@override
bool get pendingCompletePurchase => status == PurchaseStatus.purchased;
}
Original file line number Diff line number Diff line change
Expand Up @@ -240,6 +240,9 @@ abstract class InAppPurchase2API {
@async
List<SK2TransactionMessage> transactions();

@async
List<SK2TransactionMessage> unfinishedTransactions();

@async
void finish(int id);

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.6+2
version: 0.4.7

environment:
sdk: ^3.9.0
Expand Down Expand Up @@ -31,7 +31,7 @@ dev_dependencies:
flutter_test:
sdk: flutter
json_serializable: ^6.0.0
pigeon: ^26.1.0
pigeon: ^26.1.1
test: ^1.16.0

topics:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -450,6 +450,19 @@ class FakeStoreKit2Platform implements InAppPurchase2API {
]);
}

@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',
),
]);
}

@override
Future<void> startListeningToTransactions() async {
isListenerRegistered = true;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -665,4 +665,63 @@ void main() {
},
);
});

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

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

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

expect(transactions, isNotEmpty);
expect(transactions.first.receiptData, isNotNull);
expect(transactions.first.receiptData, 'fake_jws_representation');
},
);
});

group('appAccountToken exposure', () {
test('should expose appAccountToken in SK2PurchaseDetails', () async {
const String testToken = 'test-uuid-12345';
final SK2PurchaseDetails details = SK2PurchaseDetails(
productID: 'test_product',
purchaseID: '999',
verificationData: PurchaseVerificationData(
localVerificationData: '',
serverVerificationData: '',
source: 'app_store',
),
transactionDate: '2025-11-15',
status: PurchaseStatus.purchased,
appAccountToken: testToken,
);

expect(details.appAccountToken, testToken);
});

test('should handle null appAccountToken in SK2PurchaseDetails', () async {
final SK2PurchaseDetails details = SK2PurchaseDetails(
productID: 'test_product',
purchaseID: '999',
verificationData: PurchaseVerificationData(
localVerificationData: '',
serverVerificationData: '',
source: 'app_store',
),
transactionDate: '2025-11-15',
status: PurchaseStatus.purchased,
);

expect(details.appAccountToken, isNull);
});
});
}