> ## 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.

# Integration Patterns

> Choose the right way to handle offer results in your Encore integration.

## 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-result** — `await 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

### Async-result (recommended when applicable)

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

```pseudo theme={null}
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.

```pseudo theme={null}
// 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()
```

<Info>
  On every platform, **re-registering a handler replaces the previous one** — no double-firing. You can call the setters multiple times safely.
</Info>

***

## 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

| Scenario                                                               | Pattern                          |
| ---------------------------------------------------------------------- | -------------------------------- |
| `Cancel` button in your subscription settings (you have purchase code) | Async-result                     |
| Encore presented from a Superwall delegate that can't see your VM      | Handler                          |
| Multiple `show()` sites sharing one billing path                       | Handler                          |
| Single `show()` call with branch-specific UI navigation                | Async-result                     |
| Cross-cutting analytics on every offer regardless of caller            | Handler (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:

| Platform          | Wrapper                                                    |
| ----------------- | ---------------------------------------------------------- |
| Swift / SwiftUI   | `Task { ... }`                                             |
| Android / Compose | `lifecycleScope.launch { ... }` or `coroutineScope.launch` |
| Flutter           | `unawaited(...)` or `await` from an `async` callback       |
| React Native      | `await` from an `async` event handler                      |
| Web               | `await` 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

<CardGroup cols={2}>
  <Card title="iOS" icon="apple" href="/publishers/ios/guides/integration-patterns">
    Swift code for both patterns, `Task` wrapping, replace semantics
  </Card>

  <Card title="Android" icon="android" href="/publishers/android/guides/integration-patterns">
    Kotlin code, `lifecycleScope.launch`, sealed-class `PresentationResult`
  </Card>

  <Card title="React Native" icon="react" href="/publishers/react-native/guides/integration-patterns">
    TypeScript code, `PlacementResult.status`, unsubscribe semantics
  </Card>

  <Card title="Flutter" icon="flutter" href="/publishers/flutter/guides/integration-patterns">
    Dart code, `EncorePresentationResult` switch, `setOn*` naming
  </Card>

  <Card title="Web" icon="globe" href="/publishers/web/guides/integration-patterns">
    Async-result + per-placement callbacks (no global handlers)
  </Card>
</CardGroup>

***

## See also

* [iOS Quickstart — Present Offers](/publishers/ios/quickstart/present-offers)
* [Android Quickstart — Present Offers](/publishers/android/quickstart/present-offers)
* [React Native Quickstart — Present Offers](/publishers/react-native/quickstart/present-offers)
* [Flutter Quickstart — Present Offers](/publishers/flutter/quickstart/present-offers)
* [Web — Redemption Modes](/publishers/web/concepts/redemption-modes)
