Overview
@encorekit/react-native 2.x tracks the native 2.0 SDKs (EncoreKit 2.0.0 on iOS, com.encorekit:encore 2.0.1 on Android). Both natives removed the global purchase and passthrough callbacks and replaced the placement result with a factual two-funnel record, so this is a breaking release of the bridge’s public API:
- Purchases run through a purchase controller you pass to
configure(). An async function returning'purchased' | 'cancelled' | 'pending'. The SDK never runs purchase code you didn’t write. show()never rejects. Errors arrive as values on the result.- The result is a factual record, not a single verdict: two independent funnels plus how the sheet ended.
- Global
on*setters are retired. Control flow lives at the call site; passive observation moves to theEncore.onOutcomestream.
Step 1: Update the package
npx tsc --noEmit: the errors are your migration worklist, and each one corresponds to a step below. The removed API table maps every 1.x symbol to its replacement.
Step 2: Move purchases to a purchase controller
This is the largest change. In 1.x, purchases ran through theonPurchaseRequest event and you acknowledged them with a second call, completePurchaseRequest(success). 2.x replaces the pair with a single function, passed to configure(), whose return value is the answer.
Before
- The answer is a return value, not a second call. The bridge correlates each request by id internally, so a late reply can no longer land on a live flow. Nothing is left to forget, and the SDK can no longer be wedged by a missing acknowledgement.
- The result is three-valued. See
pendingis not a failure below, which is the whole reason the 1.x pair was removed rather than shimmed. - Throwing is meaningful. A thrown error records
publisher: 'failed'with your message, and the flow continues. Map your billing layer’s “user cancelled” error to'cancelled'rather than letting it throw.
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 webhook is the source of truth.
The 1.x boolean handler could not express this state at all, so every deferred purchase was reported as a failure. When you map your billing layer onto PurchaseResult, returning 'cancelled' for a deferred purchase silently misreports revenue: a sale that is still in flight is recorded as a decline, in the presentation record and in every analytics surface downstream. Route the deferred branch of your billing library to 'pending'.
Where the controller goes
The controller is aconfigure() option, matching every other Encore SDK. Binding it is configuring, so there is no window in which the SDK is configured and your purchase path is not. EncoreProvider forwards its purchaseController prop into that same call.
If the controller genuinely has to change at runtime, swap it with Encore.setPurchaseController().
Registering no controller is a supported configuration, not a broken one: the SDK never attempts a purchase, keeps its own not_attempted record and sdk_iap_no_purchase_controller diagnostic, and every presentation resolves publisher: 'not_attempted'.
Step 3: Update show() call sites
show() no longer rejects. Presentation failures arrive as { status: 'not_presented', reason: { type: 'error', error } } on the PresentationResult, so try/catch blocks around it are dead code:
Encore.show(placementId) is removed; use the builder. Encore.placements.setClaimEnabled(bool) flattened to Encore.setClaimEnabled(bool), because both natives deleted the 1.x placements manager.
Step 4: Update result handling
1.x modeled the result as a flatstatus string, which forced a claim, a purchase, and a dismissal to compete for one winning value. 2.0 keeps them apart; see PresentationResult for the full shape.
Before
unlocked boolean, because what a claim means is a property of the variant flow that served it, and an SDK verdict computed over app-global config could contradict the flow that actually ran. Branch on the raw axes.
Step 5: Replace global callbacks with the outcomes stream
onPassthrough, onPurchaseComplete and registerCallbacks() are removed.
onPurchaseComplete has no native counterpart in 2.0: the SDK no longer runs purchases itself, so there is no native purchase to report, and your controller is the code that ran the one that happened.
onPassthrough becomes a reading of the presentation record rather than an event:
Encore.onOutcome:
on* setters:
- It appends. The 1.x setters replaced the previous handler; this is a real subscription, and several observers can listen at once, matching the multicast streams both natives expose.
- There is no replay. Subscribe at startup, or you miss outcomes emitted before you subscribed.
strict_unlock_verified, which has no 1.x equivalent: a strict claim the server confirmed after its flow ended, possibly on a later launch, so it can never be a show() return value.
Removed APIs at a glance
Behavior changes to be aware of
These typecheck fine but behave differently at runtime:- Errors no longer reject. Old
catchfallback blocks are dead code; run fallback offnot_presentedinstead. - Claim-then-purchase records both funnels. 1.x collapsed the flow to a single winner. 2.0 carries
advertiser: { type: 'claimed', … }andpublisher: 'purchased'side by side, and the result resolves once, after the purchase settles. - A deferred purchase is now visible. Flows that previously recorded a failure for Ask to Buy or SCA now record
publisher: 'pending', provided your controller returns'pending'for them. useCase,headlineandsubheadlinenow actually work. The JS builder already exposed them in 1.x, but the bridge had no native method behind them, so the values were dropped with a warning andrewardUserscould never present. Both bridges now forward all three on everyshow().reset()does not clear the controller. It is build-time wiring, not user state, so logging a user out does not require re-registering.
New in 2.0
- The outcomes stream, including late
strict_unlock_verifiedevents that survive process death. - Unlock mode.
options.unlock: 'strict'verifies claims server-side, persisting unverified claims and re-checking them across launches.'optimistic'(the default, and 1.x’s behavior) records the claim and finishes. - A working reward surface.
.useCase(UseCase.rewardUsers)presents the claim-only reward sheet instead of the churn-intervention sheet, with.headline()/.subheadline()copy overrides. basePlanIdon the purchase request, naming the Google Play base plan to select when a product exposes several.
Verify the migration
Confirm each of these before shipping:
npx tsc --noEmitis clean, with no references to removed 1.x APIs.- If your Encore flows include an in-app purchase, a purchase controller is passed to
configure(), either directly or as theEncoreProviderprop. - Your controller returns
'pending'for deferred purchases, and'cancelled'only for a genuine user cancellation. - Every fallback path runs off the no-claim, no-purchase branch, 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.