Skip to main content

Overview

Every Encore SDK exposes the same primitive: Encore.placement(id).show() opens the offer sheet, the user accepts or dismisses, and you do something next. There are two idiomatic ways to wire up the “do something next” part:
  1. Async-resultawait show() returns a result; branch on it locally.
  2. Handler-based — register onPurchaseRequest / onPassthrough once at app launch; the SDK calls back into them whenever a placement resolves.
Both patterns are first-class. Pick by whether the call site that triggers show() already has access to your purchase + decline logic.

The two patterns

The trigger is also the handler. Code that should run in both branches is written once, control flow is local, no global state.

Handler-based

Register handlers once at app init. Anywhere in the app can then call show() without knowing how purchases are wired.
On every platform, re-registering a handler replaces the previous one — no double-firing. You can call the setters multiple times safely.

Decision tree

When async-result wins

  • The trigger lives next to the purchase code (e.g. a Cancel Subscription button in your settings screen that already imports your billing client)
  • Code that should run on both branches — analytics, navigation, cleanup — would otherwise duplicate across handler + caller
  • You want the post-show flow to read top-to-bottom without hopping between files

When handlers win

  • The trigger is a third-party paywall delegate (Superwall, RevenueCat) where you don’t control the call site
  • Purchase code lives in a ViewModel or DI scope not visible from a UI button action
  • Multiple show() sites share post-purchase logic and you’d rather centralize it

Mixing is fine

Handlers can fire for cross-cutting telemetry while async results drive control flow. The two patterns coexist on the same placement — the handler pattern is the default the SDK falls back to whenever the call site doesn’t act on the returned result.

Common scenarios


Cross-cutting concerns

Re-registration is replace, not append

All five SDKs replace the previous handler when you call the setter again. You don’t need to defensively unsubscribe. On React Native, the handler setters return an unsubscribe function for component-lifecycle convenience — useful but not required to prevent double-fires.

Async wrapping at non-async triggers

show() is async on every platform. When the trigger is a synchronous callback (SwiftUI Button action body, Compose onClick, an event listener) you wrap the call in the platform’s standard async launcher:

Web has no global handlers

The Web SDK doesn’t expose onPurchaseRequest / onPassthrough — browsers handle the purchase via redirects/forms, so there’s no purchase-delegation contract to register. Use the async-result of Encore.placement(id).show() for control flow, and per-placement callbacks (Encore.placement(id).onGranted(...).onNotGranted(...)) for cross-cutting telemetry.

Order of operations for handlers

If you go with handlers, register them before any show() call. The simplest place is right after configure() at app launch. Late registration risks a placement firing into the void.

Per-platform guides

iOS

Swift code for both patterns, Task wrapping, replace semantics

Android

Kotlin code, lifecycleScope.launch, sealed-class PresentationResult

React Native

TypeScript code, PlacementResult.status, unsubscribe semantics

Flutter

Dart code, EncorePresentationResult switch, setOn* naming

Web

Async-result + per-placement callbacks (no global handlers)

See also