[in_app_purchase_storekit] Add Transaction.unfinished API and expose appAccountToken (#10439)

## Summary

Adds two new StoreKit 2 features to `in_app_purchase_storekit`:

- `SK2Transaction.unfinishedTransactions()` - Queries only unfinished transactions for better performance
- `SK2PurchaseDetails.appAccountToken` - Exposes user UUID for backend integration

## Motivation

1. **Performance:** Developers often only need unfinished transactions to complete them, not all historical transactions. This mirrors Apple's official `Transaction.unfinished` API.
2. **User Identification:** The ability to set `appAccountToken` already exists when making purchases, but reading it back from transaction details was missing.

## Changes

- Added pigeon interface method for `unfinishedTransactions()`
- Implemented Swift native code using Apple's `Transaction.unfinished` API
- Exposed `appAccountToken` property in `SK2PurchaseDetails`
- Added unit tests for both features

## Breaking Changes

None. Both features are additive and maintain full backward compatibility.
diff --git a/packages/in_app_purchase/in_app_purchase_storekit/CHANGELOG.md b/packages/in_app_purchase/in_app_purchase_storekit/CHANGELOG.md
index 60407b4..22c26fd 100644
--- a/packages/in_app_purchase/in_app_purchase_storekit/CHANGELOG.md
+++ b/packages/in_app_purchase/in_app_purchase_storekit/CHANGELOG.md
@@ -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.
diff --git a/packages/in_app_purchase/in_app_purchase_storekit/darwin/in_app_purchase_storekit/Sources/in_app_purchase_storekit/StoreKit2/InAppPurchasePlugin+StoreKit2.swift b/packages/in_app_purchase/in_app_purchase_storekit/darwin/in_app_purchase_storekit/Sources/in_app_purchase_storekit/StoreKit2/InAppPurchasePlugin+StoreKit2.swift
index da8874e..f6d7299 100644
--- a/packages/in_app_purchase/in_app_purchase_storekit/darwin/in_app_purchase_storekit/Sources/in_app_purchase_storekit/StoreKit2/InAppPurchasePlugin+StoreKit2.swift
+++ b/packages/in_app_purchase/in_app_purchase_storekit/darwin/in_app_purchase_storekit/Sources/in_app_purchase_storekit/StoreKit2/InAppPurchasePlugin+StoreKit2.swift
@@ -230,6 +230,28 @@
     }
   }
 
+  /// 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
+      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 }
diff --git a/packages/in_app_purchase/in_app_purchase_storekit/darwin/in_app_purchase_storekit/Sources/in_app_purchase_storekit/StoreKit2/StoreKit2Messages.g.swift b/packages/in_app_purchase/in_app_purchase_storekit/darwin/in_app_purchase_storekit/Sources/in_app_purchase_storekit/StoreKit2/StoreKit2Messages.g.swift
index 82341c6..eba2f16 100644
--- a/packages/in_app_purchase/in_app_purchase_storekit/darwin/in_app_purchase_storekit/Sources/in_app_purchase_storekit/StoreKit2/StoreKit2Messages.g.swift
+++ b/packages/in_app_purchase/in_app_purchase_storekit/darwin/in_app_purchase_storekit/Sources/in_app_purchase_storekit/StoreKit2/StoreKit2Messages.g.swift
@@ -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
@@ -727,6 +727,8 @@
   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
@@ -860,6 +862,24 @@
     } 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)
diff --git a/packages/in_app_purchase/in_app_purchase_storekit/lib/src/sk2_pigeon.g.dart b/packages/in_app_purchase/in_app_purchase_storekit/lib/src/sk2_pigeon.g.dart
index 0d40ad3..9f0c995 100644
--- a/packages/in_app_purchase/in_app_purchase_storekit/lib/src/sk2_pigeon.g.dart
+++ b/packages/in_app_purchase/in_app_purchase_storekit/lib/src/sk2_pigeon.g.dart
@@ -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
 
@@ -978,6 +978,37 @@
     }
   }
 
+  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';
diff --git a/packages/in_app_purchase/in_app_purchase_storekit/lib/src/store_kit_2_wrappers/sk2_transaction_wrapper.dart b/packages/in_app_purchase/in_app_purchase_storekit/lib/src/store_kit_2_wrappers/sk2_transaction_wrapper.dart
index bc09ff5..55305cc 100644
--- a/packages/in_app_purchase/in_app_purchase_storekit/lib/src/store_kit_2_wrappers/sk2_transaction_wrapper.dart
+++ b/packages/in_app_purchase/in_app_purchase_storekit/lib/src/store_kit_2_wrappers/sk2_transaction_wrapper.dart
@@ -27,6 +27,7 @@
     this.subscriptionGroupID,
     this.price,
     this.error,
+    this.receiptData,
     this.jsonRepresentation,
   });
 
@@ -63,7 +64,11 @@
   /// 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]
@@ -76,7 +81,7 @@
 
   /// A wrapper around [Transaction.all]
   /// https://developer.apple.com/documentation/storekit/transaction/3851203-all
-  /// A sequence that emits all the customer’s 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
@@ -85,6 +90,18 @@
     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() {
@@ -111,6 +128,7 @@
       purchaseDate: purchaseDate,
       expirationDate: expirationDate,
       appAccountToken: appAccountToken,
+      receiptData: receiptData,
       jsonRepresentation: jsonRepresentation,
     );
   }
@@ -135,6 +153,7 @@
       // Any failed transaction will simply not be returned.
       status: restoring ? PurchaseStatus.restored : PurchaseStatus.purchased,
       purchaseID: id.toString(),
+      appAccountToken: appAccountToken,
     );
   }
 }
diff --git a/packages/in_app_purchase/in_app_purchase_storekit/lib/src/types/app_store_purchase_details.dart b/packages/in_app_purchase/in_app_purchase_storekit/lib/src/types/app_store_purchase_details.dart
index d55ca6c..8d6fdff 100644
--- a/packages/in_app_purchase/in_app_purchase_storekit/lib/src/types/app_store_purchase_details.dart
+++ b/packages/in_app_purchase/in_app_purchase_storekit/lib/src/types/app_store_purchase_details.dart
@@ -91,8 +91,13 @@
     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;
 }
diff --git a/packages/in_app_purchase/in_app_purchase_storekit/pigeons/sk2_pigeon.dart b/packages/in_app_purchase/in_app_purchase_storekit/pigeons/sk2_pigeon.dart
index ff6e42a..780ad63 100644
--- a/packages/in_app_purchase/in_app_purchase_storekit/pigeons/sk2_pigeon.dart
+++ b/packages/in_app_purchase/in_app_purchase_storekit/pigeons/sk2_pigeon.dart
@@ -241,6 +241,9 @@
   List<SK2TransactionMessage> transactions();
 
   @async
+  List<SK2TransactionMessage> unfinishedTransactions();
+
+  @async
   void finish(int id);
 
   void startListeningToTransactions();
diff --git a/packages/in_app_purchase/in_app_purchase_storekit/pubspec.yaml b/packages/in_app_purchase/in_app_purchase_storekit/pubspec.yaml
index cb642ec..5882c91 100644
--- a/packages/in_app_purchase/in_app_purchase_storekit/pubspec.yaml
+++ b/packages/in_app_purchase/in_app_purchase_storekit/pubspec.yaml
@@ -2,7 +2,7 @@
 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
@@ -31,7 +31,7 @@
   flutter_test:
     sdk: flutter
   json_serializable: ^6.0.0
-  pigeon: ^26.1.0
+  pigeon: ^26.1.1
   test: ^1.16.0
 
 topics:
diff --git a/packages/in_app_purchase/in_app_purchase_storekit/test/fakes/fake_storekit_platform.dart b/packages/in_app_purchase/in_app_purchase_storekit/test/fakes/fake_storekit_platform.dart
index 6a4e1e9..a188d3f 100644
--- a/packages/in_app_purchase/in_app_purchase_storekit/test/fakes/fake_storekit_platform.dart
+++ b/packages/in_app_purchase/in_app_purchase_storekit/test/fakes/fake_storekit_platform.dart
@@ -451,6 +451,20 @@
   }
 
   @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',
+      ),
+    ]);
+  }
+
+  @override
   Future<void> startListeningToTransactions() async {
     isListenerRegistered = true;
   }
diff --git a/packages/in_app_purchase/in_app_purchase_storekit/test/in_app_purchase_storekit_2_platform_test.dart b/packages/in_app_purchase/in_app_purchase_storekit/test/in_app_purchase_storekit_2_platform_test.dart
index 3127d15..35191b9 100644
--- a/packages/in_app_purchase/in_app_purchase_storekit/test/in_app_purchase_storekit_2_platform_test.dart
+++ b/packages/in_app_purchase/in_app_purchase_storekit/test/in_app_purchase_storekit_2_platform_test.dart
@@ -172,7 +172,7 @@
     });
 
     test(
-      'buying consumable, should get PurchaseVerificationData with serverVerificationData and localVerificationData',
+      'buying consumable, should get PurchaseVerificationData with serverVerificationData, localVerificationData, and appAccountToken',
       () async {
         final details = <PurchaseDetails>[];
         final completer = Completer<List<PurchaseDetails>>();
@@ -208,6 +208,10 @@
           result.first.verificationData.localVerificationData,
           'jsonRepresentation',
         );
+        expect(
+          (result.first as SK2PurchaseDetails).appAccountToken,
+          'appAccountToken',
+        );
       },
     );
 
@@ -658,4 +662,36 @@
       },
     );
   });
+
+  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');
+      },
+    );
+
+    test('should expose appAccountToken in unfinished transactions', () async {
+      final List<SK2Transaction> transactions =
+          await SK2Transaction.unfinishedTransactions();
+
+      expect(transactions, isNotEmpty);
+      expect(transactions.first.appAccountToken, isNotNull);
+      expect(transactions.first.appAccountToken, 'fake_app_account_token');
+    });
+  });
 }