Skip to main content

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 of configure(). EncoreProvider forwards its purchaseController prop into the configure() call it makes on mount, so the provider prop and the option are the same binding.
This is the shape that made the 1.x pair removable. In 1.x, 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.
Map your deferred branch to 'pending', not 'cancelled'. Ask to Buy (parental approval) on iOS, and SCA or a pending Play transaction on Android, mean the user has neither bought nor abandoned. The store’s webhook settles it later, possibly days later. Reporting it as a cancellation records a live sale as a decline, in the presentation record and everywhere downstream of it.
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:
One helper, used from every call site, keeps the “did Encore intercept” reading in a single place without a global handler whose firing order you have to reason about.

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.
There is no replay: subscribe at startup, or you miss outcomes emitted before you subscribed. Mount this component inside 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

Two funnels, recorded independently, because one presentation can do both. See PresentationResult for the full vocabulary of each axis.
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_foreground and iap_first_declined are Android-only; unsupported_ios, user_cancelled, last_offer_declined, and dismissed are iOS-only. Code written against the full union is portable.
  • Android’s Activity is 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