Skip to main content

Overview

encore_flutter 2.0 moves the plugin onto the native Encore 2.0 surface (EncoreKit 2.0.0 on iOS, com.encorekit:encore 2.0.1 on Android). Both natives removed the global purchase and passthrough handlers and replaced the presentation result with a factual two-funnel record, so this is a breaking release:
  • Purchases run through an EncorePurchaseController you implement and register at configure. The SDK never runs purchase code you didn’t write.
  • show() never throws. Every failure, including “nothing was presented”, is a value on the result.
  • The result is a factual record, not a verdict: two independent funnels plus how the sheet ended.
  • Encore.shared.outcomes is the passive observation channel, and the only way to see cross-launch verifications.
There is no compatibility shim for onPurchaseRequest. The new answer is three-valued and 1.x’s onPurchaseRequestResult(bool) has no way to say “deferred”. A shim would have to guess, and every guess reports an Ask to Buy or SCA purchase as either a decline or a completed sale. Silently misreporting revenue is worse than a compile error, so the 1.x API is gone rather than quietly wrong.

Step 1: Update the package

2.0 removes the 1.x surface outright rather than deprecating it, so every call site that needs attention fails to analyze. Run flutter analyze: the errors are your migration worklist, and each one maps to a step below. The removed API table maps every 1.x symbol to its replacement.

Step 2: Implement a purchase controller

This is the largest change. 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. The Dart-side API is an interface you implement and register at configure, mirroring where both natives bind it. Before
After
Two things changed beyond the shape:
  1. The controller reports an outcome. 1.x’s onPurchaseRequest could not report success at all, and onPurchaseRequestResult could only report a bool. The controller returns an EncorePurchaseResult, and an unrecognized answer throws rather than defaulting to purchased, because a phantom success grants access nobody paid for.
  2. Throwing is meaningful. A throw is recorded as EncorePublisherOutcome.failed, and the flow continues. Map your billing layer’s “user cancelled” throw to EncorePurchaseResult.cancelled rather than letting it propagate.

pending is not a failure

pending means the store deferred the purchase: Ask to Buy (parental approval) on iOS, or SCA or a pending Play transaction on Android. The user has neither bought nor abandoned. The purchase may settle minutes or days later, and the store’s eventual webhook is the source of truth. 1.x’s boolean onPurchaseRequestResult could not express this state, so it reported every deferred purchase as a failure. When you map your billing layer onto EncorePurchaseResult, returning cancelled for a deferred purchase silently misreports revenue: a sale still in flight is recorded as a decline, on the presentation record and in every analytics surface downstream. Route your billing library’s deferred branch to EncorePurchaseResult.pending.

Registration

The controller can only be registered at configure, because both native SDKs bind it once there and expose no setter afterwards. hasPurchaseController is sent explicitly, so native builds its bridge only when Dart registered one. Registering none is a supported configuration, not a broken one: the SDK never attempts a purchase at all, which is a different and more accurate behavior than registering a controller that always fails. Every presentation records EncorePublisherOutcome.notAttempted.
The controller survives reset(). It is app-level infrastructure, not user state, so logging a user out does not require re-registering it.
Android’s native controller also hands over the foreground Activity. The plugin absorbs it, since it is valid only for the duration of the call and must never be retained, and iOS has no equivalent. One Dart contract therefore covers both platforms.

Step 3: Update show() call sites and results

show() never throws. EncorePresentationResult is now a sealed pair, and the 1.x Granted / Claimed / NotGranted cases are gone. Before
After
result.claim reads the claimed offer directly when you don’t want to pattern-match the whole record:
The record carries raw facts only. There is deliberately no SDK-computed “unlocked” verdict, because what a claim means is a property of the variant flow that served it, and a projection over app-global config can contradict the flow that actually ran.
EncoreNotPresented is not a synonym for failure. It also covers noOffers, experimentControl, and useCaseUnavailable, all healthy states that still need your original flow to continue.
Every decoder maps an unrecognized value to an explicit unknown rather than to a neighbouring case, so a newer native can never be silently misread as notAttempted.

Step 4: Replace onPassthrough and onPurchaseComplete

Both are removed, and each was verified absent from both native SDKs before deletion.
  • onPurchaseComplete, along with EncoreBillingPurchaseResult, is gone: the SDK no longer runs purchases itself, so there is no native purchase to report, and your controller already sees every purchase it runs.
  • onPassthrough becomes a reading of the record. “Encore did not intercept” is now:
For cross-cutting observation, subscribe to the outcomes stream instead:
The stream is multicast and has no replay: subscribe at startup, or you miss outcomes emitted before you subscribed. It earns its place through EncoreStrictUnlockVerified, which resolves after its flow ended, possibly on a later launch, and so can never be a show() return value. EncoreUnlockMode.strict on configure is what makes that event reachable.

Removed APIs at a glance


Behavior changes to be aware of

These analyze fine but behave differently at runtime:
  • Errors no longer throw. Old try/catch fallback blocks around show() are dead code; run fallback off EncoreNotPresented instead.
  • Claim-then-purchase records both funnels. 1.x collapsed the flow to a single winner. 2.0 carries the advertiser claim and publisher: purchased side by side.
  • A deferred purchase is now visible, provided your controller returns EncorePurchaseResult.pending for it.
  • EncoreUseCase now actually reaches the native SDK. In 1.x the value was dropped at the bridge, because neither native plugin implemented the channel method carrying it, so EncoreUseCase.rewardUsers could never present and headline / subheadline overrides were silently ignored. Both bridges now forward all three on every show(), which also retires the use_case_unsupported degradation path.

New in 2.0

  • The outcomes stream, including late EncoreStrictUnlockVerified events that survive process death.
  • EncoreUnlockMode. strict verifies claims server-side and persists unverified claims across launches; optimistic (the default, and 1.x’s behavior) records the claim and finishes.
  • A working reward surface, claim-only, which never falls back to the churn-intervention sheet.
  • EncoreClaimedOffer carrying campaignId, advertiserName, and the transactionId that joins a claim to its later verification.

Verify the migration

Confirm each of these before shipping:
  1. flutter analyze is clean, with no references to removed 1.x APIs.
  2. If your Encore flows include an in-app purchase, an EncorePurchaseController is passed to configure.
  3. Your controller returns pending for deferred purchases, and cancelled only for a genuine user cancellation.
  4. Every fallback path runs off the no-claim, no-purchase reading, not a catch block.
  5. Test the full flow on a device on both platforms: present a placement, claim an offer, complete a purchase, and dismiss without acting. Each path should resolve exactly once with the result you expect.

If you hit anything this guide doesn’t cover, reach out to admin@encorekit.com.