Dunfey · Hotel WWDC as data, est. 1983
Front desk everything
Years
Topics

2024 App ServicesApp Store, Distribution & Marketing

WWDC24 · 33 min · App Services / App Store, Distribution & Marketing

Implement App Store Offers

Learn how to engage customers with App Store Offers using App Store Connect, as well as the latest StoreKit features and APIs. Discover how you can set up win-back offers (a new way to re-engage previous subscribers) and generate offer codes for Mac apps. And find out how to test offers in sandbox and Xcode to make sure they work smoothly.

Watch at developer.apple.com ↗

Transcript all transcripts

Chapters

Code shown on screen · 4 snippets

Present offer code redemption sheet on macOS - SwiftUI API swift · at 4:25 ↗
// Present offer code redemption sheet on macOS - SwiftUI API

import SwiftUI
import StoreKit

struct MyView: View {

    @State var showOfferCodeRedemption: Bool = false

    var body: some View {
        Button("Redeem Code") {
            showOfferCodeRedemption = true
        }
        .offerCodeRedemption(isPresented: $showOfferCodeRedemption) { result in
            // Handle result
        }
    }
}
Choose preferred offer in a SubscriptionStoreView swift · at 20:15 ↗
// Choose preferred offer in a SubscriptionStoreView

import SwiftUI
import StoreKit

struct MyView: View {
    let groupID: String
    
    var body: some View {
        SubscriptionStoreView(groupID: groupID)
            .preferredSubscriptionOffer { product, subscription, eligibleOffers in
                let freeTrialOffer = eligibleOffers
                    .filter { $0.paymentMode == .freeTrial }
                    .max { lhs, rhs in
                        lhs.period.value < rhs.period.value
                    }
                return freeTrialOffer ?? eligibleOffers.first
            }
    }
}
Check subscription entitlement and offer eligibility swift · at 23:05 ↗
// Check subscription entitlement and offer eligibility

import StoreKit

func shouldShowMerchandising(
    for groupID: String,
    productIDs: [Product.ID]
) async throws -> MerchandisingVisibility {
    // Get subscription status
    let statuses = try await Product.SubscriptionInfo.status(for: groupID)
    
    // Check if the customer is already entitled to the subscription
    let entitlement = SubscriptionEntitlement(for: statuses)
    if entitlement.autoRenewalEnabled {
        return .hidden
    }
    
    // Check for offers to show in merchandising UI
    let products = try await Product.products(for: productIDs)
    
    let isEligibleForIntroOffer = await Product.SubscriptionInfo.isEligibleForIntroOffer(for: groupID)
    if isEligibleForIntroOffer {
        let subscriptions = products.map {
            ($0, $0.subscription?.introductoryOffer)
        }
        return .visible(subscriptions)
    }
    
    // Check for eligible win-back offers
    let purchasedStatus = statuses.first {
        $0.transaction.unsafePayloadValue.ownershipType == .purchased
    }
    let renewalInfo = try purchasedStatus?.renewalInfo.payloadValue
    let bestWinBackOfferID = renewalInfo?.eligibleWinBackOfferIDs.first
    
    // Return the product with the offer if there is one
    if let bestWinBackOfferID {
        let subscriptions: [(Product, Product.SubscriptionOffer?)] = products.map {
            let winBackOffer = $0.subscription?.winBackOffers.first {
                $0.id == bestWinBackOfferID
            }
            return ($0, winBackOffer)
        }
        return .visible(subscriptions)
    }
    
    // Only return the product if there is no offer
    return .visible(products.map { ($0, nil) })
}

struct SubscriptionEntitlement {
    let isEntitled: Bool
    let autoRenewalEnabled: Bool
    
    init(for statuses: [Product.SubscriptionInfo.Status]) {
        let entitledStatuses = statuses.filter {
            $0.state == .subscribed || $0.state == .inBillingRetryPeriod || $0.state == .inGracePeriod
        }
        isEntitled = !entitledStatuses.isEmpty
        autoRenewalEnabled = entitledStatuses.contains {
            $0.renewalInfo.unsafePayloadValue.willAutoRenew
        }
    }
}

enum MerchandisingVisibility {
    case hidden
    case visible([(Product, Product.SubscriptionOffer?)])
}
Add a win-back offer to a purchase swift · at 25:26 ↗
// Add a win-back offer to a purchase

import StoreKit

func purchase(
    _ product: Product,
    with offer: Product.SubscriptionOffer?
) async throws {
    // Prepare the purchase options
    var purchaseOptions: Set<Product.PurchaseOption> = []
    
    // Add win-back offer to the purchase
    if let offer, offer.type == .winBack {
        purchaseOptions.insert(.winBackOffer(offer))
    }
    
    // Make the purchase
    try await product.purchase(options: purchaseOptions)
}

Resources