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

Dart cannot implement a Swift protocol or a Kotlin interface, so each native plugin owns the conformance and forwards over the method channel, suspending until Dart answers. What you implement is a plain Dart interface, registered once.
Registration happens at configure and nowhere else, because both native SDKs bind the controller once there and expose no setter. It survives reset().
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, on the presentation record and everywhere downstream of it. This is exactly what 1.x’s boolean onPurchaseRequestResult forced, and the reason it was removed rather than shimmed.
The bridge refuses to guess: an answer it does not recognize throws rather than defaulting to purchased, since a phantom success grants access nobody paid for. Registering none is supported: the SDK never attempts a purchase at all, and every presentation records EncorePublisherOutcome.notAttempted.

2. Control flow at the call site

show() never throws, so there is no try/catch to write and no case where your fallback silently fails to run.

When the widget 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:
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

Unlike the 1.x setOn* setters, which replaced the previous handler, this is a broadcast stream: several listeners can subscribe at once, matching the AsyncStream (iOS) and SharedFlow (Android) it bridges. There is no replay. The native streams buffer nothing for late subscribers, by design, since outcomes can carry user-scoped claim data. Subscribe at startup if you want all of them. The stream is also the only way to see EncoreStrictUnlockVerified, a strict-mode claim the server confirmed after its flow ended, possibly on a later launch. No show() return value can carry that, which is the stream’s reason for existing. It requires EncoreUnlockMode.strict on configure; under the default optimistic it never fires.

Reading the record

Dart 3 sealed classes give you an exhaustive switch, and the compiler tells you when you’ve forgotten a branch. Fact readers (result.claim, result.advertiser, result.publisher, result.dismissal) let a call site that cares about one axis skip the pattern match; they return null when nothing was presented, which is a different fact from notAttempted.
EncoreNotPresented is not a synonym for “something went wrong”. It also covers noOffers, experimentControl, and useCaseUnavailable, all 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 reason.
The publisher, dismissal, and not-presented enums map an unrecognized wire value to an explicit unknown case. An unrecognized advertiser outcome reads as EncoreAdvertiserNotAttempted.

Platform-specific notes

  • Native renders the UI. SwiftUI on iOS, Compose on Android. The plugin is a thin bridge over platform channels.
  • Some axis values are platform-specific. noForegroundActivity and iapFirstDeclined are Android-only; unsupportedOS, userCancelled, lastOfferDeclined, and dismissed are iOS-only. Code written against the full enum is portable.
  • Android’s Activity is absorbed by the plugin, not surfaced to Dart. It is valid only for the duration of the call and must never be retained, so one Dart contract covers both platforms.
  • Encore.placement(id) is static. Both Encore.placement('id') and Encore.shared.placement('id') work; the instance form is deprecated.

See also