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

# Targeting Moments

> Choose the right moments in your app to present Encore retention offers.

## Overview

Encore placements let you present targeted offers at key decision points in your app. Each moment has different user intent and conversion characteristics. Start with the highest-impact moment and expand from there.

***

## Paywall Abandonment (Primary)

The highest-impact moment. A user has seen your paywall and is about to leave without purchasing. Present an Encore offer as a last chance to convert.

```swift theme={null}
// When the user dismisses your paywall
func onPaywallDismissed() {
    Encore.placement("paywall").show()
}
```

**Why this works:**

* User has already shown purchase intent by viewing the paywall
* They've seen your pricing — an offer feels like a personalized win
* Highest conversion rate of any placement moment

**When to trigger:**

* User taps "X" or swipes to dismiss the paywall
* User taps "Not now" or "Maybe later"
* User navigates back from a pricing screen

<Tip>
  This is the recommended starting point for all new integrations. Get this working first, measure results, then expand to other moments.
</Tip>

***

## Churn Intercept (Secondary)

Catch users who are actively trying to cancel their subscription. Present a retention offer before they complete cancellation.

```swift theme={null}
// When the user taps "Cancel Subscription"
Button("Cancel Subscription") {
    Encore.placement("cancellation_flow").show()
}
```

**Why this works:**

* User is an existing subscriber — they've already paid before
* High willingness to stay if given the right incentive
* Prevents revenue loss from churn

**When to trigger:**

* User taps a cancel/unsubscribe button in your settings
* User navigates to subscription management with cancel intent
* Before redirecting to the App Store subscription management page

<Info>
  Your `onPassthrough` handler is critical here — if the user dismisses the offer, you must let them proceed with cancellation. Never block the cancel flow.
</Info>

***

## Choosing a Placement ID

Placement IDs identify where in your app the offer was triggered. Use descriptive, consistent names:

| Moment              | Placement ID        | Description                     |
| :------------------ | :------------------ | :------------------------------ |
| Paywall abandonment | `paywall`           | User dismissed the paywall      |
| Churn intercept     | `cancellation_flow` | User initiated cancellation     |
| Feature gate        | `feature_gate`      | User hit a premium feature wall |
| Upgrade prompt      | `upgrade`           | User shown an upgrade path      |

Placement IDs are used in your Encore dashboard to track conversion by moment and optimize offer targeting.

***

## Handling Results

Regardless of which moment you target, the same handlers apply:

```swift theme={null}
// Handle purchases (registered once at app init)
Encore.shared.onPurchaseRequest { purchaseRequest in
    // Purchase the product through your subscription manager.
    // Return true on success so Encore dismisses the offer sheet.
    let didSucceed = try await yourManager.purchase(purchaseRequest.productId)
    return didSucceed
}

// Handle dismissals (registered once at app init)
Encore.shared.onPassthrough { placementId in
    // Resume the user's original flow
    // e.g., proceed with cancellation, dismiss paywall, etc.
}
```

See [onPurchaseRequest](../sdk-reference/on-purchase-request) and [onPassthrough](../sdk-reference/on-passthrough) for full API details.

***

## Promotional Offer Handling

For retention moments like churn intercept, Encore can include a promotional offer with the purchase request. When a promotional offer is configured in your [Encore dashboard](https://dashboard.encorekit.com), the `PurchaseRequest` object will contain a `promoOfferId` alongside the `productId`. Your `onPurchaseRequest` handler must apply this offer during purchase.

<Info>
  Promotional offers are configured in App Store Connect and linked through your Encore dashboard. The `promoOfferId` is only present when Encore determines the user should receive a promotional offer — standard purchases will have `promoOfferId` set to `nil`.
</Info>

How you handle the promotional offer depends on your subscription manager:

<Tabs>
  <Tab title="StoreKit">
    When no `onPurchaseRequest` handler is set, Encore handles promotional offer signing and purchase automatically through native StoreKit. This is the recommended approach for apps using StoreKit directly.

    To enable automatic signing, provide your In-App Purchase key in the [Encore dashboard](https://dashboard.encorekit.com) under **Entitlements** → **Promotional Offer Setup**. See [Create a Promotional Offer](./create-promotional-offer#step-2-generate-an-in-app-purchase-key) for setup instructions.

    <Warning>
      If you set a custom `onPurchaseRequest` handler, Encore will **not** sign offers automatically. You must handle promo offer signing yourself using StoreKit's `Product.SubscriptionOffer` APIs. Only set a custom handler if your app requires it (e.g., when routing purchases to a third-party manager).
    </Warning>
  </Tab>

  <Tab title="RevenueCat">
    Manually check for the promotional offer, sign it through RevenueCat, and fall back to a standard purchase if the user isn't eligible.

    ```swift theme={null}
    Encore.shared.onPurchaseRequest { purchaseRequest in
        guard let product = try await Purchases.shared.products([purchaseRequest.productId]).first else {
            return false
        }
        if let promoOfferId = purchaseRequest.promoOfferId,
            let promoDiscount = product.discounts.first(where: { $0.offerIdentifier == promoOfferId }) {
            do {
                let offer = try await Purchases.shared.promotionalOffer(
                    forProductDiscount: promoDiscount, product: product
                )
                let result = try await Purchases.shared.purchase(product: product, promotionalOffer: offer)
                return !result.userCancelled
            } catch {
                // Promo failed (ineligible, network error, etc.) — fall back to standard purchase
                let result = try await Purchases.shared.purchase(product: product)
                return !result.userCancelled
            }
        } else {
            let result = try await Purchases.shared.purchase(product: product)
            return !result.userCancelled
        }
    }
    ```

    <Warning>
      Your local StoreKit configuration file must be synced with App Store Connect. Promotional offers will not work if it is out of sync.
    </Warning>
  </Tab>

  <Tab title="Adapty">
    Adapty automatically presents the promotional offer if the user is eligible (when configured correctly in the Adapty dashboard). Falls back to a standard purchase with an introductory offer.

    ```swift theme={null}
    Encore.shared.onPurchaseRequest { purchaseRequest in
        let paywall = try await Adapty.getPaywall(placementId: "YOUR_PAYWALL_PLACEMENT_ID")
        let products = try await Adapty.getPaywallProducts(paywall: paywall)
        guard let product = products.first(where: {
            $0.vendorProductId == purchaseRequest.productId
        }) else {
            return false
        }
        let result = try await Adapty.makePurchase(product: product)
        return !result.isPurchaseCancelled
    }
    ```

    <Warning>
      Adapty does not fall back to StoreKit — products and offers must exist in Adapty. Sync products in the Adapty dashboard before use, and configure the promotional offer there (it does not sync from App Store Connect automatically).
    </Warning>
  </Tab>

  <Tab title="Qonversion">
    Manually resolve the promotional discount from the Qonversion product catalog and apply it at purchase time. Falls back to a standard purchase if the promo is unavailable.

    ```swift theme={null}
    Encore.shared.onPurchaseRequest { purchaseRequest in
        guard let products = try? await Qonversion.shared().products(),
        let product = products.first(where: { $0.value.storeID == purchaseRequest.productId })?.value else {
            return false
        }

        if let promoOfferId = purchaseRequest.promoOfferId,
          let discount = product.skProduct?.discounts.first(where: { $0.identifier == promoOfferId }),
          let promoOffer = try? await withCheckedThrowingContinuation({ (c: CheckedContinuation<Qonversion.PromotionalOffer, Error>) in
              Qonversion.shared().getPromotionalOffer(for: product, discount: discount) { offer, err in
                  if let offer { c.resume(returning: offer) }
                  else { c.resume(throwing: err ?? NSError(domain: "Qonversion", code: -1)) }
              }
          }) {
          let opts = Qonversion.PurchaseOptions()
          opts.promoOffer = promoOffer
          let result = try await Qonversion.shared().purchaseProduct(product, options: opts)
          return !result.isCancelled
        } else {
          let result = try await Qonversion.shared().purchaseProduct(product)
          return !result.isCancelled
        }
    }
    ```

    <Warning>
      Qonversion does not fall back to StoreKit — products must be synced first. Click "Sync from Stores" in the Qonversion dashboard to import your products before using promotional offers.
    </Warning>
  </Tab>
</Tabs>

***

## Next Steps

* [Present Offers (Quickstart)](../quickstart/present-offers) — Basic integration walkthrough
* [Using RevenueCat](./using-revenuecat) — RevenueCat-specific purchase handling
* [Using Superwall](./using-superwall) — Superwall-specific purchase handling
* [Using Qonversion](./using-qonversion) — Qonversion-specific purchase handling
