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 ↗Code shown on screen · 11 snippets
Create a listener for promoted in-app purchases
// 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
// 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
// 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
// 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)
// 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
// 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
// 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
// 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
// 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
// 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
// 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
Related sessions
-
37 min -
13 min -
20 min -
48 min -
35 min -
38 min