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

# EncorePurchaseController

> The purchase controller your app registers so Encore offers purchase through your billing code

All purchases triggered by Encore offers run through an `EncorePurchaseController` your app registers at [`configure()`](./configure) time. The SDK has no built-in billing and never runs purchase code you did not write: your controller owns receipt validation, restore handling, and entitlement bookkeeping, exactly as it does for every other purchase in your app.

## Definition

```swift theme={null}
@MainActor
public protocol EncorePurchaseController: AnyObject {
    /// Called when Encore's offer flow needs a purchase. Perform it through
    /// your subscription manager and report what happened.
    func purchase(_ request: PurchaseRequest) async throws -> EncorePurchaseResult
}

public enum EncorePurchaseResult: Sendable {
    case purchased
    case cancelled
    case pending      // deferred (Ask to Buy / SCA); may complete later
}
```

Return `.purchased` on success, `.cancelled` when the user backs out, and `.pending` for deferred flows; throw for real failures. The result lands on the record as [`PublisherOutcome`](./presentation-result#publisheroutcome), and the controller is awaited inside the sheet lifetime, so the sheet reacts to the outcome, except on IAP-first variants, where the purchase runs before the sheet is presented.

## PurchaseRequest

What your controller receives when Encore needs a purchase.

```swift theme={null}
public struct PurchaseRequest: Sendable {
    public let productId: String       // IAP product to purchase
    public let placementId: String?    // the placement that triggered it, if any
    public let promoOfferId: String?   // reserved; always nil on iOS 2.0
}
```

| Property       | Type      | Description                                                                                                                                                                       |
| -------------- | --------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `productId`    | `String`  | The IAP product identifier to purchase, for example `com.app.monthly_premium`                                                                                                     |
| `placementId`  | `String?` | Which placement triggered this purchase, if any                                                                                                                                   |
| `promoOfferId` | `String?` | Reserved for Encore-driven promotional offers, which iOS SDK 2.0 does not yet support. Always `nil`; see [Create a Promotional Offer](../platform-setup/create-promotional-offer) |

## Implementations

<Tabs>
  <Tab title="StoreKit 2">
    ```swift theme={null}
    final class AppPurchases: EncorePurchaseController {
        func purchase(_ request: PurchaseRequest) async throws -> EncorePurchaseResult {
            let products = try await Product.products(for: [request.productId])
            guard let product = products.first else { throw PurchaseError.notFound }
            switch try await product.purchase() {
            case .success(let verification):
                if case .verified(let transaction) = verification { await transaction.finish() }
                return .purchased
            case .userCancelled: return .cancelled
            case .pending: return .pending
            @unknown default: return .cancelled
            }
        }
    }
    ```
  </Tab>

  <Tab title="RevenueCat">
    ```swift theme={null}
    final class AppPurchases: EncorePurchaseController {
        func purchase(_ request: PurchaseRequest) async throws -> EncorePurchaseResult {
            let products = try await Purchases.shared.products([request.productId])
            guard let product = products.first else { throw PurchaseError.notFound }
            let result = try await Purchases.shared.purchase(product: product)
            return result.userCancelled ? .cancelled : .purchased
        }
    }
    ```
  </Tab>

  <Tab title="Adapty">
    ```swift theme={null}
    final class AppPurchases: EncorePurchaseController {
        func purchase(_ request: PurchaseRequest) async throws -> EncorePurchaseResult {
            let products = try await Adapty.getPaywallProducts(paywall: paywall)
            guard let product = products.first(where: { $0.vendorProductId == request.productId }) else {
                throw PurchaseError.notFound
            }
            let result = try await Adapty.makePurchase(product: product)
            return result.isPurchaseCancelled ? .cancelled : .purchased
        }
    }
    ```
  </Tab>

  <Tab title="Custom">
    ```swift theme={null}
    final class AppPurchases: EncorePurchaseController {
        func purchase(_ request: PurchaseRequest) async throws -> EncorePurchaseResult {
            // request.productId    - App Store product ID to purchase
            // request.placementId  - which placement triggered this (optional)
            // request.promoOfferId - reserved; always nil on iOS 2.0
            let didSucceed = try await yourManager.purchase(request.productId)
            return didSucceed ? .purchased : .cancelled
        }
    }
    ```
  </Tab>
</Tabs>

## Related

* [configure()](./configure), which accepts the controller and defines what happens when none is registered.
* [PresentationResult](./presentation-result), where the controller's result lands as `PublisherOutcome`.
* [Configure the SDK](../quickstart/configure#register-a-purchase-controller) for the registration walkthrough.
