Skip to main content

Overview

SDK 2.0 is a breaking release that makes the integration surface simpler and harder to misuse:
  • show() returns a result and never throws. Errors arrive as values on the result instead of as thrown exceptions.
  • Purchases run through a registered EncorePurchaseController. The SDK never runs purchase code you didn’t write.
  • Global handler closures and delegates are retired. Results are values at the call site; passive observation moves to the Encore.shared.outcomes stream.
  • The public API is @MainActor. Results and callbacks always land on the main actor.

Step 1: Update the package

In Xcode, select the Encore package under Package Dependencies and change its version requirement to Up to Next Major Version from 2.0.0. In a Package.swift manifest:
2.0 removes the 1.x legacy surface outright rather than deprecating it, so every call site that needs attention fails to compile. Build the project: the compile errors that appear are your migration worklist, and each one corresponds to a step below. Work through them until the project builds, then run the verification checklist. The removed API table maps every 1.x symbol to its replacement.

Step 2: Register a purchase controller

This is the largest change. In 1.x, purchases ran through the onPurchaseRequest closure, and if no handler was registered the SDK bought the product itself through StoreKit and reported it via onPurchaseComplete. 2.0 removes the closure, the completion callback, and the built-in StoreKit fallback: that fallback bypassed whatever receipt validation, restore handling, and entitlement bookkeeping your app already has, and it made a missing registration look like a working purchase instead of a misconfiguration. Conform a small class to EncorePurchaseController and pass it to configure(...):
Return .purchased on success, .cancelled when the user backs out, and .pending for deferred flows such as Ask to Buy; throw for real failures.
The controller registration is build-time wiring, not user state: it survives reset(), so logging a user out does not require re-registering.
If your app uses Encore offers without a subscription product, no controller is needed and no purchase is ever attempted. If a product is configured but no controller is registered, the claim completes with publisher: .notAttempted, the SDK logs a warning naming the product, and the sdk_iap_no_purchase_controller analytics event makes the misconfiguration visible.

Step 3: Update show() call sites

show() no longer throws. Errors arrive as .notPresented(.error(EncoreError)) on the PresentationResult, so do/catch blocks around it are dead code:
Fire-and-forget presentation becomes show(resume:), which delivers every outcome (including .notPresented) on the main actor:
The public API is now @MainActor. Calls from a background queue or a nonisolated helper no longer compile; move them to the main actor. SwiftUI actions, .task blocks, and UIKit handlers are already there.

Step 4: Update result handling

1.x modeled the result as a flat enum (.purchased, .granted(...), .notGranted(reason)), which forced a claim and a purchase to compete for a single winning case. 2.0 splits the result into .notPresented(reason) and .presented(Outcome); see PresentationResult for the full shape. The record carries raw facts only: 2.0 removes the SDK-computed “unlocked” verdict, so your integration decides what a claim means. Most 1.x switches collapse to a one-line check on the facts:
result.claim surfaces the claimed offer (campaignId, advertiserName, transactionId) whenever the claim funnel reached .claimed or .verified; read result.advertiser directly to distinguish the two. result.publisher reports what your purchase code returned. When the mechanism detail matters, switch on the record:
The 1.x result never carried the entitlement payload correctly across modes, so 2.0 removes it from the result entirely. The granted entitlement is always read from isActive / isActivePublisher, the authoritative state.

Step 5: Replace global callbacks with the outcomes stream

onPassthrough, EncoreDelegate, encoreSheet(onGranted:), and the builder and manager onGranted / onNotGranted callbacks are removed. Handle fallback inline at the call site (Step 4), and move passive observation (analytics, logging, cross-cutting state) to the Encore.shared.outcomes stream:

Removed APIs at a glance

The deprecated typealiases NotGrantedReason and EncorePresentationResult survive as zero-cost renames that Xcode fix-its resolve.

Behavior changes to be aware of

These compile fine but behave differently at runtime:
  • Errors no longer throw from show(). Old catch fallback blocks are dead code; run fallback off .notPresented instead.
  • Claim-then-purchase records both funnels. 1.x collapsed the flow to a single winner. 2.0 carries advertiser: .claimed(...) and publisher: .purchased side by side, and the result is delivered once, after the purchase resolves (later than 1.x fired its callbacks).
  • Strict mode records the claim, and verification upgrades it. The record says advertiser: .claimed(...) immediately (and result.claim surfaces it); in-session verification upgrades it to .verified(...), and late completion arrives as .strictUnlockVerified on Encore.shared.outcomes, surviving process death. Gate concrete access on isActive / isActivePublisher.
  • Callbacks and resumes always land on the main actor. 1.x invoked them from arbitrary contexts.

New in 2.0, with no migration required

Existing call sites keep working unchanged. These arrive on top:
  • A claim-only reward surface. A placement can select .useCase(.rewardUsers) to present after the user has already done something worth celebrating, such as a completed purchase, a milestone, or a streak. It never runs a purchase and never falls back to the paywall sheet. Placements that omit useCase(_:) keep the default .reduceChurn surface.
  • Per-presentation copy overrides. headline(_:) and subheadline(_:) replace the sheet’s shipped copy for one presentation. You pass the whole string, so localization and pluralization stay yours.
  • A runtime claim gate. Encore.shared.isClaimEnabled dims the claim CTA and stops it responding to taps, leaving the rest of the presentation intact.
  • No more wedged presentations. A dead flow can no longer brick presentation with .alreadyPresenting: every flow resolves on a real event.
  • Durable strict-mode claims. A strict-mode claim that ends unverified persists across launches until verification completes.

Verify the migration

Confirm each of these before shipping:
  1. The project compiles with no references to removed 1.x APIs.
  2. If a subscription product is configured for your app, an EncorePurchaseController is registered at configure time.
  3. Every fallback path (your original paywall or flow) runs off the no-claim, no-purchase branch (result.claim == nil && result.publisher != .purchased), not a catch block.
  4. Test the full flow on a device: present a placement, claim an offer, complete a purchase, and dismiss without acting. Each path should resolve exactly once with the result you expect.

If you hit anything this guide doesn’t cover, reach out to admin@encorekit.com.