Repository navigation
Description
Steps to reproduce
in_app_purchase_storekit: ^0.4.8+1 and in_app_purchase: ^3.2.3
1.Enable StoreKit2 path
2.Start listening to the purchase stream in Dart
InAppPurchase.instance.purchaseStream.listen((purchases) {
// log or handle purchase events here
});3.Trigger a StoreKit2 purchase
final result = await InAppPurchase.instance.buyNonConsumable(
purchaseParam: purchaseParam,
);Expected results
Expected single-purchase flow:
After calling purchase(id:options:):
You’ll receive only one “purchase successful” event. The business side can safely proceed with shipping based on this event (e.g., awarding in-game coins).
Transaction.updates is used for:
Handling scenarios involving purchases made outside the app, on other devices, or through restore functionality;
or handling historical/unfinished transactions.
It will not send another .purchased event for the same purchase within the current session that duplicates the one sent by purchase().
Expected status semantics:
status: .purchased indicates:
“This transaction has already been completed on the Store side and can be considered final at the game’s business layer, allowing for safe shipment.”
If certain callbacks merely indicate that “the transaction has been created/updated on the Store side but still requires a secondary confirmation UI logic within the game,” then these callbacks should have distinct semantics (or clearly documented instructions requiring the business layer to handle idempotency themselves).
Actual results
Under the StoreKit2 mode, the same purchase will trigger two calls to sendTransactionUpdate(..., status: .purchased):
- First time: Inside the function "purchase(id:options:)"
let result = try await product.purchase(options: purchaseOptions)
switch result {
case .success(let verification):
sendTransactionUpdate(
productId: id,
transaction: verification.unsafePayloadValue,
receipt: verification.jwsRepresentation,
status: .purchased
)
case .pending:
sendTransactionUpdate(productId: id, status: .pending)
case .userCancelled:
sendTransactionUpdate(productId: id, status: .cancelled)
@unknown default:
fatalError("An unknown StoreKit PurchaseResult has been encountered.")
}- Second time: Within the Transaction.updates event listened for in the startListeningToTransactions() method
func startListeningToTransactions() throws {
self.setListenerTaskAsTask(
task: Task { [weak self] in
for await verificationResult in Transaction.updates {
switch verificationResult {
case .verified(let transaction):
self?.sendTransactionUpdate(
productId: transaction.productID,
transaction: transaction,
receipt: verificationResult.jwsRepresentation,
status: .purchased
)
case .unverified:
break
}
}
})
}The purchaseStream on the Dart side (or the upper-level IAP management logic) will receive two nearly identical "purchased" events:
If the business logic is "ship the goods and add gold whenever status == purchased", then the same transaction will be shipped twice.
A more crucial point is:
In actual experience, the second .purchased callback from Transaction.updates sometimes triggers before the "final confirmation dialog" of our own game:
From a business perspective, this event is more similar to the "purchasing" or "pending transactions" in SK1;
However, it is currently uniformly labeled as ".purchased", and it is difficult for the business side to distinguish between "final success for shipment" and "updates during the intermediate process" solely based on the "status".
Code sample
Code sample
// lib/src/in_app_purchase_storekit_platform.dart
if (_useStoreKit2) {
final updateController2 =
StreamController<List<PurchaseDetails>>.broadcast(
onListen: () => SK2Transaction.startListeningToTransactions(),
onCancel: () => SK2Transaction.stopListeningToTransactions(),
);
_sk2transactionObserver = SK2TransactionObserverWrapper(
transactionsCreatedController: updateController2,
);
InAppPurchase2CallbackAPI.setUp(_sk2transactionObserver);
} else {
// SK1 ...
}Screenshots or Video
Screenshots / Video demonstration
[Upload media here]
Logs
Logs
[Paste your logs here]Flutter Doctor output
Doctor output
[✓] Flutter (Channel stable, 3.32.7, on macOS 26.3.1 25D2128 darwin-arm64, locale zh-Hans-CN)
[✓] Android toolchain - develop for Android devices (Android SDK version 35.0.1)
[✓] Xcode - develop for iOS and macOS (Xcode 26.2)
[✓] Chrome - develop for the web
[✓] Android Studio (version 2025.2)
[✓] VS Code (version 1.111.0)
[✓] Connected device (7 available)
! Error: Browsing on the local area network for Ricky’s iPhone. Ensure the device is unlocked and attached with a cable or
associated with the same local area network as this Mac.
The device must be opted into Developer Mode to connect wirelessly. (code -27)
! Error: Browsing on the local area network for iPhone. Ensure the device is unlocked and attached with a cable or associated with
the same local area network as this Mac.
The device must be opted into Developer Mode to connect wirelessly. (code -27)
! Error: Browsing on the local area network for 文涵Team ipad. Ensure the device is unlocked and attached with a cable or associated
with the same local area network as this Mac.
The device must be opted into Developer Mode to connect wirelessly. (code -27)
! Error: Browsing on the local area network for chuanzhe2’s iPhone. Ensure the device is unlocked and attached with a cable or
associated with the same local area network as this Mac.
The device must be opted into Developer Mode to connect wirelessly. (code -27)
! Error: Browsing on the local area network for xuebin000. Ensure the device is unlocked and attached with a cable or associated
with the same local area network as this Mac.
The device must be opted into Developer Mode to connect wirelessly. (code -27)
! Error: Browsing on the local area network for chuanzhe. Ensure the device is unlocked and attached with a cable or associated with
the same local area network as this Mac.
The device must be opted into Developer Mode to connect wirelessly. (code -27)
[✓] Network resources
• No issues found!
Activity
[-][StoreKit2] purchase() and Transaction.updates both call sendTransactionUpdate(.purchased) causing duplicate “purchased” events and early purchased state[/-][+][in_app_purchase_storekit] purchase() and Transaction.updates both call sendTransactionUpdate(.purchased) causing duplicate “purchased” events and early purchased state[/+]saurabh-mirajkar commentedon Mar 16, 2026
@LatCui Thanks for the report.
To help investigate further, could you please provide:
flutter doctor -v)in_app_purchaseplugin versionAlso, parts of the issue description are not in English. Per the Flutter issue hygiene guidelines, please consider updating the report in English so more contributors can help investigate:
https://github.com/flutter/flutter/blob/master/docs/contributing/issue_hygiene/README.md#consider-posting-issues-in-english
github-actions commentedon Apr 6, 2026
Without additional information, we are unfortunately not sure how to resolve this issue. We are therefore reluctantly going to close this bug for now.
If you find this problem please file a new issue with the same description, what happens, logs and the output of 'flutter doctor -v'. All system setups can be slightly different so it's always better to open new issues and reference the related ones.
Thanks for your contribution.
github-actions commentedon Apr 20, 2026
This thread has been automatically locked since there has not been any recent activity after it was closed. If you are still experiencing a similar issue, please open a new bug, including the output of
flutter doctor -vand a minimal reproduction of the issue.