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

2023 App Store, Distribution & MarketingApp Services

WWDC23 · 24 min · App Store, Distribution & Marketing / App Services

What’s new in StoreKit 2 and StoreKit Testing in Xcode

Get to know the latest enhancements to StoreKit 2 and StoreKit Testing in Xcode. Discover API updates for promoted in-app purchases, StoreKit messages, the Transaction model, the RenewalInfo model, and the App Store sheet for managing subscriptions. Learn how to upgrade to SHA-256 for on-device receipt validation and use APIs to create SwiftUI views. We’ll also help you get started with StoreKit Testing in Xcode so that you can debug and test your in-app purchases and subscriptions. Meet the Transaction Inspector, explore the latest updates to the StoreKit configuration editor, and find out how you can simulate StoreKit errors to test your app’s error handling.

Watch at developer.apple.com ↗

Transcript all transcripts

Code shown on screen · 11 snippets

Create a listener for promoted in-app purchases swift · at 1:42 ↗
// Create a listener for promoted in-app purchases
import StoreKit

let promotedPurchasesListener = Task {
    for await promotion in PurchaseIntent.intents {
        // Process promotion
        let product = promotion.product

        // Purchase promoted product
        do {
            let result = try await product.purchase()
            // Process result
        }
        catch {
            // Handle error
        }
    }
}
Check promotion order swift · at 2:57 ↗
// Check promotion order
import StoreKit

do {
    let promotions = try await Product.PromotionInfo.currentOrder

    if promotions.isEmpty {
        // No local promotion order set
    }

    for promotion in promotions {
        let productID = promotion.productID
        let productVisibility = promotion.visibility
        // Check promoted products
    }
}
catch {
    // Handle error
}
Set a promotion order swift · at 3:26 ↗
// Set a promotion order
import StoreKit

let newPromotionOrder: [String] = [
    "acorns.individual",
    "nectar.cup",
    "sunflowerseeds.pile"
]

do {
    try await Product.PromotionInfo.updateProductOrder(byID: newPromotionOrder)
}
catch {
    // Handle error
}
Update promotion visibility swift · at 4:02 ↗
// Update promotion visibility
import StoreKit

// Hide “acorns.individual”
do {
    try await Product.PromotionInfo.updateProductVisibility(.hidden, for: "acorns.individual")
}
catch {
    // Handle error
}
Update promotion visibility (alternative method) swift · at 4:17 ↗
// Update promotion visibility
import StoreKit

do {
  let promotions = try await Product.PromotionInfo.currentOrder

  // Hide the first product
  if var firstPromotion = promotions.first {
    firstPromotion.visibility = .hidden
    try await firstPromotion.update()
  }
}
catch {
  // Handle error
}
Product view swift · at 8:32 ↗
// Product view
import SwiftUI
import StoreKit

struct BirdFoodShop: View {
    let productID: String
    let productImage: String

    var body: some View {
        ProductView(id: productID) {
            BirdFoodProductIcon(for: productID)
        }
        .productViewStyle(.large)
    }
}
Store view swift · at 8:52 ↗
// Store view
import SwiftUI
import StoreKit

struct BirdFoodShop: View {
    let productIDs: [String]

    var body: some View {
        StoreView(ids: productIDs) { product in
            BirdFoodIcon(productID: product.id)
        }
    }
}
Subscription view swift · at 9:19 ↗
// Subscription view
import SwiftUI
import StoreKit

struct BackyardBirdsPassShop: View {
    let groupID: String

    var body: some View {
        SubscriptionStoreView(groupID: groupID)
    }
}
Simulated off-device purchase using StoreKitTest swift · at 21:09 ↗
// Simulated off-device purchase using StoreKitTest
import StoreKit
import StoreKitTest

func testSubscriptionRenewal() async throws {
    let session = try SKTestSession(configurationFileNamed: "Store")

    let oneYearInterval: TimeInterval = (365 * 24 * 60 * 60)
    let transaction = try await session.buyProduct(
        identifier: "birdpass.individual",
        options: [
            .purchaseDate(Date.now - oneYearInterval)
        ]
    )

    // Inspect transaction
}
Set a simulated purchase error when loading products swift · at 21:48 ↗
// Set a simulated purchase error when loading products
import StoreKit
import StoreKitTest

func testLoadProducts() async throws {
    let session = try SKTestSession(configurationFileNamed: "Store")
    let productIDs = [
        "acorns.individual",
        "nectar.cup"
    ]

    // Set a simulated error, then load products, expecting an error
    session.setSimulatedError(.generic(.networkError), forAPI: .loadProducts)
    do {
        _ = try await Product.products(for: productIDs)
        XCTFail("Expected a network error")
    }
    catch StoreKitError.networkError(_) {
        // Expected error thrown, continue...
    }
    // Disable simulated error
    session.setSimulatedError(nil, forAPI: .loadProducts)
}
Set a faster subscription renewal rate in a test session swift · at 22:24 ↗
// Set a faster subscription renewal rate in a test session
import StoreKit
import StoreKitTest

func testSubscriptionRenewal() async throws {
    let session = try SKTestSession(configurationFileNamed: "Store")

    // Set renewals to expire every minute
    session.timeRate = .oneRenewalEveryMinute

    let transaction = try await session.buyProduct(identifier: "birdpass.individual")

    // Wait for renewals and inspect transactions
}

Resources