Skip to main content

Purpose

Create and present Encore offers using a fluent, optional-callback API that fits naturally in SwiftUI and UIKit. Supports both per-placement callbacks and global listeners for centralized event handling.

Signature

public static func placement(_ id: String? = nil) -> Placement

public struct Placement {
  public func onGranted(_ callback: @escaping (Entitlement) -> Void) -> Placement
  public func onNotGranted(_ callback: @escaping (NotGrantedReason) -> Void) -> Placement
  public func onLoadingStateChange(_ callback: @escaping (Bool) -> Void) -> Placement
  public func show()
  public func show() async throws -> EncorePresentationResult
}

Placement Methods

MethodParametersDescription
placement()id: String?Creates a placement with optional identifier. If omitted, auto-generates unique ID
onGranted()Entitlement -> VoidOptional. Sets callback invoked when user accepts an offer
onNotGranted()NotGrantedReason -> VoidOptional. Sets callback invoked when entitlement not granted (user declined or error)
onLoadingStateChange()Bool -> VoidOptional. Sets callback to track loading state
show()NonePresents the offer sheet modally (callback-based)
show() asyncNonePresents the offer and returns EncorePresentationResult or throws EncoreError

Returns / State

placement() returns a Placement instance you can chain with optional callbacks. Call show() to present.

Global Placement Listeners

Global listeners provide centralized event handling for all placement presentations across your app. These are invoked for every placement presentation, making them ideal for analytics, logging, or cross-cutting concerns.
MethodParametersDescription
Encore.placements.onGranted()(String, Entitlement) -> VoidSets global callback invoked whenever any placement grants an entitlement. Receives placement ID and entitlement. Returns PlacementsManager for chaining
Encore.placements.onNotGranted()(String, NotGrantedReason) -> VoidSets global callback invoked whenever any placement does not grant an entitlement. Receives placement ID and reason. Returns PlacementsManager for chaining

Signature

public static var placements: PlacementsManager { get }

public class PlacementsManager {
  public func onGranted(_ callback: @escaping (String, Entitlement) -> Void) -> PlacementsManager
  public func onNotGranted(_ callback: @escaping (String, NotGrantedReason) -> Void) -> PlacementsManager
}
Global listeners are invoked in addition to per-placement callbacks. Use global listeners for analytics and centralized logic, while per-placement callbacks handle specific flow control.

Usage Patterns

Global Listeners for Analytics

Set up global listeners once during app initialization to track all placement events:
import Encore

@main
struct YourApp: App {
  init() {
    Encore.shared.configure(apiKey: "pk_your_api_key")
    
    // Set up global listeners for analytics (chainable)
    Encore.placements
      .onGranted { placementId, entitlement in
        Analytics.track("encore_granted", properties: [
          "placement_id": placementId,
          "entitlement_type": "\(entitlement)",
          "timestamp": Date()
        ])
      }
      .onNotGranted { placementId, reason in
        Analytics.track("encore_not_granted", properties: [
          "placement_id": placementId,
          "reason": "\(reason)",
          "timestamp": Date()
        ])
      }
  }
  
  var body: some Scene {
    WindowGroup {
      ContentView()
    }
  }
}
Global listeners receive the placement ID, allowing you to differentiate between different placements in your app (e.g., “cancellation_flow” vs “onboarding_gate”). They are perfect for cross-cutting concerns like analytics, logging, or state synchronization that should happen for every placement presentation.

Cancellation Flow with Named Placement

Button("Cancel Subscription") {
  Encore.placement("cancellation_flow")
    .onLoadingStateChange { isLoading in
      self.showSpinner = isLoading
    }
    .onGranted { entitlement in
      print("User accepted: \(entitlement)")
    }
    .onNotGranted { reason in
      // Proceed with cancellation flow
      proceedWithCancellation()
    }
    .show()
}
Provide a placement ID to identify this specific placement in your global listeners and analytics.

Async/Await Pattern

Button("Cancel Subscription") {
  Task {
    do {
      let result = try await Encore.placement("cancellation_flow").show()
      switch result {
      case .granted(let entitlement):
        print("User accepted: \(entitlement)")
      case .notGranted(let reason):
        print("Not granted: \(reason)")
        proceedWithCancellation()
      }
    } catch {
      // Only system errors are thrown
      handleError(error as? EncoreError ?? .unknown)
    }
  }
}

Passive Offer (No Callbacks)

Button("See Offer") {
  // Just show the offer - entitlement state managed automatically via isActivePublisher
  Encore.placement("upsell_modal").show()
}
When you don’t need custom flow control, you can omit callbacks entirely. The SDK automatically manages entitlement state through isActivePublisher().