Skip to main content

Documentation Index

Fetch the complete documentation index at: https://docs.encorekit.com/llms.txt

Use this file to discover all available pages before exploring further.

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.
async function onCancelTapped() {
    let result = await Encore.placement("cancel_flow").show()
    switch (result) {
        case .granted:    // user accepted the offer
            navigate(.home)
        case .notGranted: // user declined or no offers
            proceedWithCancellation()
    }
    // Code that runs in both branches
    analytics.track("cancel_flow_resolved")
}

Handler-based

Register handlers once at app init. Anywhere in the app can then call show() without knowing how purchases are wired.
// At app launch, after Encore.configure(...)
Encore.shared.onPurchaseRequest { request in
    await myBilling.purchase(request.productId)
}
Encore.shared.onPassthrough { placementId in
    proceedWithDefaultFlow(placementId)
}

// Anywhere, anytime
Encore.placement("cancel_flow").show()
On every platform, re-registering a handler replaces the previous one — no double-firing. You can call the setters multiple times safely.

Decision tree

Does the show() call site have direct access to your purchase logic
AND your decline/continuation logic?

├── YES -> Async-result (RECOMMENDED)
│         let result = await Encore.placement(id).show()
│         switch result { case .granted: ... case .notGranted: ... }

└── NO  -> Handler pattern
          - Register Encore.shared.onPurchaseRequest at app init
          - Register Encore.shared.onPassthrough at app init
          - Guarantee handlers are registered BEFORE any show() call

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

ScenarioPattern
Cancel button in your subscription settings (you have purchase code)Async-result
Encore presented from a Superwall delegate that can’t see your VMHandler
Multiple show() sites sharing one billing pathHandler
Single show() call with branch-specific UI navigationAsync-result
Cross-cutting analytics on every offer regardless of callerHandler (alongside async-result)

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:
PlatformWrapper
Swift / SwiftUITask { ... }
Android / ComposelifecycleScope.launch { ... } or coroutineScope.launch
Flutterunawaited(...) or await from an async callback
React Nativeawait from an async event handler
Webawait from an async function

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