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
EncorePurchaseControlleryou implement and register atconfigure. 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.outcomesis the passive observation channel, and the only way to see cross-launch verifications.
Step 1: Update the package
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 atconfigure, mirroring where both natives bind it.
Before
- The controller reports an outcome. 1.x’s
onPurchaseRequestcould not report success at all, andonPurchaseRequestResultcould only report abool. The controller returns anEncorePurchaseResult, and an unrecognized answer throws rather than defaulting topurchased, because a phantom success grants access nobody paid for. - Throwing is meaningful. A throw is recorded as
EncorePublisherOutcome.failed, and the flow continues. Map your billing layer’s “user cancelled” throw toEncorePurchaseResult.cancelledrather 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 atconfigure, 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
result.claim reads the claimed offer directly when you don’t want to pattern-match the whole record:
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.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 withEncoreBillingPurchaseResult, 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.onPassthroughbecomes a reading of the record. “Encore did not intercept” is now:
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/catchfallback blocks aroundshow()are dead code; run fallback offEncoreNotPresentedinstead. - Claim-then-purchase records both funnels. 1.x collapsed the flow to a single winner. 2.0 carries the advertiser claim and
publisher: purchasedside by side. - A deferred purchase is now visible, provided your controller returns
EncorePurchaseResult.pendingfor it. EncoreUseCasenow 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, soEncoreUseCase.rewardUserscould never present andheadline/subheadlineoverrides were silently ignored. Both bridges now forward all three on everyshow(), which also retires theuse_case_unsupporteddegradation path.
New in 2.0
- The outcomes stream, including late
EncoreStrictUnlockVerifiedevents that survive process death. EncoreUnlockMode.strictverifies 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.
EncoreClaimedOffercarryingcampaignId,advertiserName, and thetransactionIdthat joins a claim to its later verification.
Verify the migration
Confirm each of these before shipping:
flutter analyzeis clean, with no references to removed 1.x APIs.- If your Encore flows include an in-app purchase, an
EncorePurchaseControlleris passed toconfigure. - Your controller returns
pendingfor deferred purchases, andcancelledonly for a genuine user cancellation. - Every fallback path runs off the no-claim, no-purchase reading, not a
catchblock. - 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.