Summary:
This article explains key behavioral details of Unity IAP v5, including how core callbacks such as OnProductsFetched, OnPurchasesFetched, OnStoreDisconnected, and purchase lifecycle events (deferred, pending, failed) are triggered. It clarifies handling of failed confirmations, v4 pending purchases, and how SCA/Ask to Buy are surfaced. It also covers recommended integration patterns, including why IAP v5 is designed for global event-based handling and how to wrap it safely with TaskCompletionSource and test Product instances.
Symptoms:
- “I want to know how often OnProductsFetched and OnPurchasesFetched are called in IAP v5.”
- “Is OnStoreDisconnected raised only on initial connect failure or also on later disconnects?”
- “How are CheckEntitlements, pending purchases, SCA, and Ask to Buy handled in v5?”
- “Is it safe to wrap IStoreService/IProductService/IPurchaseService callbacks with TaskCompletionSource?”
- “How can I construct Product instances for testing?”
Cause:
The questions arise from changes and clarifications in Unity IAP v5’s event model, purchase lifecycle, and backward compatibility with v4 pending purchases. Developers integrating IAP v5 need to understand how callbacks are triggered, how different pending and failure scenarios are surfaced, and how to adapt event-driven APIs to async/await patterns and unit testing needs without conflicting with the global, event-based design of the package.
Resolution:
OnPurchasesFetched is invoked once per FetchPurchases() call and when RestoreTransactions() succeeds (from IAP v5.0.2).
- OnProductsFetched is invoked once per successful FetchProducts() call; multiple calls trigger the event multiple times.
- OnStoreDisconnected is raised both when the initial connect() fails and whenever the store connection is later lost (network/service issues).
- CheckEntitlements checks all subscriptions and non-consumables the user currently owns.
- Pending purchase flow:
- When a purchase enters a platform-level pending state (e.g., SCA, Ask to Buy), OnPurchaseDeferred is invoked with a DeferredOrder.
- If later approved, OnPurchasePending is invoked (not OnPurchaseSucceeded).
- If later denied, OnPurchaseFailed is invoked.
- Failed order scenarios include:
- Confirming the same PendingOrder multiple times.
- Store FinishTransaction() failures (receipt validation, network errors, etc.).
In these cases, inspect failedOrder.FailureReason and failedOrder.Details to decide whether to retry or treat as a permanent failure. Do not grant in-game content if confirmation fails, because the transaction may not be finalized with the store.
- IAP v5 automatically handles v4 pending purchases; no separate v4 restore flow is required.
- SCA and Ask to Buy are treated identically by StoreKit and Unity IAP: both trigger OnPurchaseDeferred and follow the same deferred → pending/failed flow. Unity cannot distinguish between them at SDK level.
- Wrapping IAP v5 with TaskCompletionSource is acceptable but should respect the global event-based design:
- IAP v5 events are global; if multiple purchases run concurrently, all registered handlers fire for every purchase.
- Use global event handlers combined with a mapping structure (e.g., a dictionary keyed by product ID or order ID) to associate each incoming Order with the correct TaskCompletionSource (queue/promise pattern).
- Avoid per-operation local event subscriptions that assume a single active purchase.
- For testing Product-related logic:
- ProductDefinition and ProductMetadata have public constructors and can be instantiated directly for test data:
- Create a ProductDefinition with an ID and ProductType.
- Create a ProductMetadata with price, title, description, ISO currency code, and decimal price.
- Then construct a Product using the internal constructor if you expose internals to the test assembly (via InternalsVisibleTo).
- Alternatively, use the internal static factory Product.CreateUnknownProduct(string productId), which creates a Product with ProductType.Unknown, again requiring InternalsVisibleTo for test assemblies.
- ProductDefinition and ProductMetadata have public constructors and can be instantiated directly for test data: