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.outcomesstream. - 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 from2.0.0. In a Package.swift manifest:
Step 2: Register a purchase controller
This is the largest change. In 1.x, purchases ran through theonPurchaseRequest 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(...):
.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.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:
show(resume:), which delivers every outcome (including .notPresented) on the main actor:
@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(). Oldcatchfallback blocks are dead code; run fallback off.notPresentedinstead. - Claim-then-purchase records both funnels. 1.x collapsed the flow to a single winner. 2.0 carries
advertiser: .claimed(...)andpublisher: .purchasedside 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 (andresult.claimsurfaces it); in-session verification upgrades it to.verified(...), and late completion arrives as.strictUnlockVerifiedonEncore.shared.outcomes, surviving process death. Gate concrete access onisActive/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 omituseCase(_:)keep the default.reduceChurnsurface. - Per-presentation copy overrides.
headline(_:)andsubheadline(_:)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.isClaimEnableddims 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:
- The project compiles with no references to removed 1.x APIs.
- If a subscription product is configured for your app, an
EncorePurchaseControlleris registered atconfiguretime. - Every fallback path (your original paywall or flow) runs off the no-claim, no-purchase branch (
result.claim == nil && result.publisher != .purchased), not acatchblock. - 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.