Overview
SDK 2.0 replaced the 1.x choice between “async result” and “global handlers” with three pieces that each have one job. There is no pattern to pick: the question is only where each piece lives in your app.
The rule of thumb: control flow at the call site, observation on the stream, purchases in the controller. Nothing in 2.0 requires a global handler to run your fallback.
1. The purchase controller
One binding, at the root, as part ofconfigure(). EncoreProvider forwards its purchaseController prop into the configure() call it makes on mount, so the provider prop and the option are the same binding.
onPurchaseRequest fired an event and you answered with a second call, completePurchaseRequest(success); forgetting one code path left the SDK wedged, and the boolean could not express a deferred purchase at all. Now the answer is the return value, the bridge correlates requests by id, and there is nothing to forget.
Registering none is a supported configuration: nothing is ever charged, and every presentation records publisher: 'not_attempted'.
2. Control flow at the call site
show() never rejects, so there is no catch to write and no case where your fallback silently fails to run.
When the call site can’t see your continuation logic
In 1.x this was the reason to reach for global handlers. In 2.0, keep the call site local and hand the decision outward, rather than handing the SDK a global callback:3. Observation on the stream
onOutcome appends, unlike the 1.x on* setters, which replaced the previous handler. Several observers can listen at once, matching the multicast streams both native SDKs expose.
EncoreProvider, above your navigator.
The stream is also the only way to see strict_unlock_verified, a strict-mode claim the server confirmed after its flow ended, possibly on a later launch. No show() return value can carry that.
Reading the record
not_presented is not a synonym for “something went wrong”. It also covers no_offer_available, experiment_control, and use_case_unavailable, all of which are healthy states that still need your original flow to continue. Run your fallback off the whole “no claim, no purchase” reading, not off the error branch.Platform-specific notes
- The offer UI is native. SwiftUI on iOS, Jetpack Compose on Android. The bridge triggers presentation and returns the record.
- Some axis values are platform-specific.
no_foregroundandiap_first_declinedare Android-only;unsupported_ios,user_cancelled,last_offer_declined, anddismissedare iOS-only. Code written against the full union is portable. - Android’s
Activityis absorbed by the bridge, not leaked to JavaScript. It has no JavaScript representation, and your billing library resolves the current Activity itself, so one controller contract covers both platforms. reset()keeps the controller. It is build-time wiring, not user state.
See also
- Quickstart: Present Offers
- Updating to 2.x, if you are migrating an existing 1.x integration
- SDK Reference:
placement(),PurchaseController,PresentationResult,onOutcome()