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

# PresentationResult

> The factual record of a placement presentation: two independent funnels plus how the sheet ended

What [`show()`](./placement) resolves to. Either nothing ever appeared, or the interaction ran and the record carries **two independent funnels** plus how the sheet went away.

`show()` never rejects. Presentation-level failures arrive here as values.

## Structure

```tsx theme={null}
type PresentationResult =
  | { status: 'not_presented'; reason: NotPresentedReason }
  | {
      status: 'presented';
      advertiser: AdvertiserOutcome;   // Encore's funnel: how far the claim got
      publisher: PublisherOutcome;     // your funnel: what your controller reported
      dismissal: DismissReason;        // how the sheet went away
      claim: ClaimedOffer | null;      // shorthand for a claimed/verified offer
    };
```

The two funnels are independent axes, not competing cases: a single presentation can claim an offer **and** run a purchase, and 2.0 records both. `not_attempted` is a real value, meaning "the funnel was open and nothing entered it", never a missing one.

<Note>
  **There is no `unlocked` boolean, on purpose.** What a claim means is a property of the variant flow that served it, so an SDK-computed verdict over app-global config could contradict the flow that actually ran. The record is facts; the policy is yours.
</Note>

## Reading it

```tsx theme={null}
const result = await Encore.placement('cancel_flow').show();

if (result.status === 'presented') {
  const converted = result.claim !== null || result.publisher === 'purchased';
  if (converted) grantAccess();
  else proceedWithCancellation();
} else {
  proceedWithCancellation();   // nothing was shown; result.reason says why
}
```

When the mechanism matters, branch on the axes directly:

```tsx theme={null}
if (result.status === 'presented') {
  analytics.track('encore_presentation', {
    advertiser: result.advertiser.type,
    publisher: result.publisher,
    dismissal: result.dismissal,
  });
}
```

## PublisherOutcome

Your funnel: what your [purchase controller](./purchase-controller) reported back.

```tsx theme={null}
type PublisherOutcome =
  | 'not_attempted'
  | 'purchased'
  | 'cancelled'
  | 'pending'
  | 'failed';
```

| Value           | Meaning                                                                                               |
| --------------- | ----------------------------------------------------------------------------------------------------- |
| `not_attempted` | No purchase was attempted. Also what a presentation records when no purchase controller is registered |
| `purchased`     | Your controller returned `'purchased'`                                                                |
| `cancelled`     | Your controller returned `'cancelled'`                                                                |
| `pending`       | Your controller returned `'pending'`: the store deferred the purchase and it may settle later         |
| `failed`        | Your controller threw                                                                                 |

<Warning>
  Treat `pending` as neither a conversion nor an abandonment. It is Ask to Buy (parental approval) on iOS, or SCA or a pending Play transaction on Android: the user has not been charged and has not walked away, and the store's webhook is the source of truth. Folding it into `cancelled` in your own reporting reproduces exactly the misreporting the 1.x boolean handler caused.
</Warning>

## AdvertiserOutcome

Encore's funnel: how far the offer claim got.

```tsx theme={null}
type AdvertiserOutcome =
  | { type: 'not_attempted' }
  | { type: 'claimed'; offer: ClaimedOffer }
  | { type: 'verified'; offer: ClaimedOffer }
  | { type: 'cooldown' }
  | { type: 'failed'; error: EncoreErrorInfo };
```

| Case            | Meaning                                                                    |
| --------------- | -------------------------------------------------------------------------- |
| `not_attempted` | The claim funnel was open and nothing entered it                           |
| `claimed`       | The claim completed on-device; server verification not (yet) observed      |
| `verified`      | Server-level confirmation was observed in-session                          |
| `cooldown`      | The claim was rejected server-side by the cross-offer provisional cooldown |
| `failed`        | The claim errored. An SDK-side failure the user never saw                  |

`result.claim` is shorthand for the offer behind a `claimed` or `verified` outcome, and `null` otherwise. Read `result.advertiser` directly when you need to tell the two apart.

### ClaimedOffer

```tsx theme={null}
interface ClaimedOffer {
  offerId: string;
  campaignId: string;
  advertiserName: string;
  transactionId?: string;
}
```

On iOS an offer *is* a campaign, so `offerId` equals `campaignId` there. `transactionId` joins this record to a later [`strict_unlock_verified`](./outcomes) outcome and to the server-side completion event that can land days afterwards; it is nullable, because the transaction write can fail while the claim still happened.

## DismissReason

How a presented sheet went away. These strings are the frozen cross-platform vocabulary, and the same values land in `decline_reason` on `sdk_offer_sheet_dismissed`.

```tsx theme={null}
type DismissReason =
  | 'close_button'
  | 'swipe_dismiss'
  | 'user_cancelled'
  | 'last_offer_declined'
  | 'dismissed'
  | 'provisional_cooldown'
  | 'flow_completed'
  | 'interrupted';
```

| Value                  | Meaning                                                        | Platform |
| ---------------------- | -------------------------------------------------------------- | -------- |
| `close_button`         | The user tapped the close control                              | Both     |
| `swipe_dismiss`        | The user swiped the sheet away                                 | Both     |
| `user_cancelled`       | The user cancelled out of the flow                             | iOS      |
| `last_offer_declined`  | The user declined the final offer in the flow                  | iOS      |
| `dismissed`            | Generic dismissal                                              | iOS      |
| `provisional_cooldown` | The claim was rejected server-side by the cross-offer cooldown | Both     |
| `flow_completed`       | The flow finished on its own and the SDK closed the sheet      | Both     |
| `interrupted`          | The SDK force-ended a dead flow, not the user                  | Both     |

## NotPresentedReason

Why no sheet ever appeared.

```tsx theme={null}
type NotPresentedReason =
  | {
      type:
        | 'not_configured'
        | 'already_presenting'
        | 'unsupported_ios'
        | 'no_offer_available'
        | 'experiment_control'
        | 'use_case_unavailable'
        | 'no_foreground'
        | 'iap_first_declined';
    }
  | { type: 'error'; error: EncoreErrorInfo };
```

| Value                  | Meaning                                                                                    | Platform |
| ---------------------- | ------------------------------------------------------------------------------------------ | -------- |
| `not_configured`       | A method was called before `configure()`                                                   | Both     |
| `already_presenting`   | A presentation is already on screen                                                        | Both     |
| `unsupported_ios`      | The device is below the SDK's minimum OS version                                           | iOS      |
| `no_offer_available`   | No offers matched this user. Don't retry immediately                                       | Both     |
| `experiment_control`   | Control cohort: exposure logged, no UI by design                                           | Both     |
| `use_case_unavailable` | The requested use case resolved no layout, typically because it isn't enabled for this app | Both     |
| `no_foreground`        | No foreground Activity was available when the SDK needed one                               | Android  |
| `iap_first_declined`   | An IAP-first layout ran its purchase before any UI and the user declined                   | Android  |
| `error`                | Network, decoding, or integration failure. See [Errors](./errors)                          | Both     |

<Note>
  `use_case_unavailable` is a correct no-op, not an error. It never falls back to the churn-intervention sheet, because putting an in-app-purchase screen in front of a user who was meant to be rewarded is worse than presenting nothing.
</Note>

## Related

* [placement()](./placement), which returns this record.
* [PurchaseController](./purchase-controller), whose answer becomes `publisher`.
* [outcomes](./outcomes), which carries this record for passive observers.
* [Errors](./errors) for the `EncoreErrorInfo` shape.
